├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sagar
│ │ └── fluenty
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── audio.mp3
│ ├── java
│ │ └── com
│ │ │ └── sagar
│ │ │ └── fluenty
│ │ │ ├── MainActivity.kt
│ │ │ └── ui
│ │ │ ├── managers
│ │ │ ├── AudioPlayerManager.kt
│ │ │ ├── AudioRecorderManager.kt
│ │ │ ├── EncryptedSharedPreferencesManager.kt
│ │ │ ├── GeminiApiManager.kt
│ │ │ ├── SpeechRecognizerManager.kt
│ │ │ └── TextToSpeechManager.kt
│ │ │ ├── screen
│ │ │ ├── audio
│ │ │ │ ├── AudioRecordScreen.kt
│ │ │ │ └── AudioRecordScreenViewModel.kt
│ │ │ ├── conversation
│ │ │ │ ├── ConversationScreen.kt
│ │ │ │ └── ConversationScreenViewModel.kt
│ │ │ ├── home
│ │ │ │ └── HomeScreen.kt
│ │ │ └── settings
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ └── SettingsScreenViewModel.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ └── utils
│ │ │ ├── ComposeUtils.kt
│ │ │ └── ErrorResponse.kt
│ └── res
│ │ ├── drawable
│ │ ├── english.png
│ │ ├── english_literature.jpg
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── logo.jpg
│ │ ├── mic.png
│ │ └── pronunciation.jpg
│ │ ├── mipmap-anydpi-v26
│ │ └── ic_launcher.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── sagar
│ └── fluenty
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fluenty: Your AI-Powered English Assistant
2 |
3 | ## **Key Features: **
4 |
5 | * **English Speaking Practice:** Enhance your fluency and confidence with AI feedback and suggestions. Start a normal conversation and AI will make sure your grammar is correct.
6 |
7 |
8 | https://github.com/user-attachments/assets/b5f7e629-c9c0-445a-b06e-b9825c7d0709
9 |
10 |
11 | * **Pronunciation Practice:** Improve your pronunciation with AI-powered feedback, Random words will be suggested to pronounce and AI will give you feedback with a score.
12 |
13 |
14 | https://github.com/user-attachments/assets/2005223d-a4a6-4258-99f2-1915c41e00f5
15 |
16 |
17 | * **Multi-Model Support:** Option to select different Gemini Models from Setting. Note that Different models can generate whole different responses.
18 |
19 | > NOTE: I am using Gemini API to generate responses, and the correctness of the responses is completely dependent on the Model.
20 |
21 | ## **Getting Started:**
22 |
23 | 1. **Download:** Download the latest APK from this [Link](https://github.com/Sagar0-0/Fluenty/releases/download/1.0.0/Fluenty.apk).
24 | 2. **Install:** Install the APK on your Android device.
25 | 3. **Add API Key:** Go to settings and Add your Gemini test API key(Generate if you don't have it).
26 |
27 | ## **Usage:**
28 |
29 | 1. **Launch the App:** Open the Fluenty app.
30 | 2. **Start Practicing:** Begin practicing your English speaking and pronunciation skills.
31 |
32 |
33 | ## **Technical Stack/Libraries/APIs:**
34 |
35 | * **Jetpack Compose:** Modern UI toolkit for building Android apps.
36 | * **MediaPlayer:** MediaPlayer class can be used to control the playback of audio/ video files and streams.
37 | * **MediaRecorder:** Used to record audio and video.
38 | * **EncryptedSharedPreferences:** An implementation of SharedPreferences that encrypts keys and values.
39 | * **GenerativeModel:** A facilitator for a given multimodal model. (Model used: "Gemini-1.5-pro-002")
40 | * **SpeechRecognizer:** Recognizes User Speech and returns a String Result.
41 | * **TextToSpeech:** Uses Android TTS service and reads the AI responses.
42 | * **Coroutines:** Asynchronous programming framework for non-blocking operations.
43 |
44 | ## **Contributing:**
45 |
46 | We welcome contributions to improve Fluenty. Feel free to fork the repository and submit pull requests.
47 |
48 | **License:**
49 |
50 | This project is licensed under the MIT License.
51 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.kotlin.android)
6 | alias(libs.plugins.kotlin.compose)
7 | }
8 |
9 | android {
10 | namespace = "com.sagar.fluenty"
11 | compileSdk = 35
12 |
13 | defaultConfig {
14 | applicationId = "com.sagar.fluenty"
15 | minSdk = 24
16 | targetSdk = 34
17 | versionCode = 1
18 | versionName = "1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | debug {
32 | buildConfigField(
33 | "String", "GEMINI_API_KEY_DEBUG",
34 | gradleLocalProperties(
35 | projectRootDir = rootDir,
36 | providers = rootProject.providers
37 | ).getProperty("GEMINI_API_KEY_DEBUG")
38 | )
39 | }
40 | }
41 | compileOptions {
42 | sourceCompatibility = JavaVersion.VERSION_11
43 | targetCompatibility = JavaVersion.VERSION_11
44 | }
45 | kotlinOptions {
46 | jvmTarget = "11"
47 | }
48 | buildFeatures {
49 | compose = true
50 | buildConfig = true
51 | }
52 | }
53 |
54 | dependencies {
55 |
56 | implementation(libs.androidx.core.ktx)
57 | implementation(libs.androidx.lifecycle.runtime.ktx)
58 | implementation(libs.androidx.activity.compose)
59 | implementation(platform(libs.androidx.compose.bom))
60 | implementation(libs.androidx.ui)
61 | implementation(libs.androidx.ui.graphics)
62 | implementation(libs.androidx.ui.tooling.preview)
63 | implementation(libs.androidx.material3)
64 |
65 | implementation(libs.androidx.navigation.compose)
66 |
67 | testImplementation(libs.junit)
68 | androidTestImplementation(libs.androidx.junit)
69 | androidTestImplementation(libs.androidx.espresso.core)
70 | androidTestImplementation(platform(libs.androidx.compose.bom))
71 | androidTestImplementation(libs.androidx.ui.test.junit4)
72 | debugImplementation(libs.androidx.ui.tooling)
73 | debugImplementation(libs.androidx.ui.test.manifest)
74 |
75 | // Ai
76 | implementation(libs.generativeai)
77 |
78 |
79 | //SharedPreference
80 | implementation(libs.androidx.security.crypto)
81 |
82 | // Gson
83 | implementation(libs.gson)
84 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/sagar/fluenty/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.sagar.fluenty", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/assets/audio.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/assets/audio.mp3
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.navigation.compose.NavHost
8 | import androidx.navigation.compose.composable
9 | import androidx.navigation.compose.rememberNavController
10 | import com.sagar.fluenty.ui.screen.audio.AudioRecordScreen
11 | import com.sagar.fluenty.ui.screen.conversation.ConversationScreen
12 | import com.sagar.fluenty.ui.screen.home.HomeScreen
13 | import com.sagar.fluenty.ui.screen.settings.SettingsScreen
14 | import com.sagar.fluenty.ui.theme.FluentyTheme
15 |
16 | class MainActivity : ComponentActivity() {
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | enableEdgeToEdge()
20 | setContent {
21 | FluentyTheme {
22 | val navController = rememberNavController()
23 | NavHost(
24 | navController = navController,
25 | startDestination = "HOME"
26 | ) {
27 | composable("HOME") {
28 | HomeScreen(
29 | onConversationClick = {
30 | navController.navigate("CONVERSATION")
31 | },
32 | onAudioClick = {
33 | navController.navigate("AUDIO")
34 | },
35 | onSettingsClick = {
36 | navController.navigate("SETTINGS")
37 | }
38 | )
39 | }
40 | composable("CONVERSATION") {
41 | ConversationScreen {
42 | navController.navigateUp()
43 | }
44 | }
45 | composable("AUDIO") {
46 | AudioRecordScreen {
47 | navController.navigateUp()
48 | }
49 | }
50 | composable("SETTINGS") {
51 | SettingsScreen {
52 | navController.navigateUp()
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/AudioPlayerManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.content.Context
4 | import android.media.MediaPlayer
5 | import androidx.core.net.toUri
6 | import java.io.File
7 |
8 | interface AudioPlayerManager {
9 | fun initListener(listener: AudioPlayerListener)
10 | fun play(file: File)
11 | fun stop()
12 | }
13 |
14 | class AudioPlayerManagerImpl(
15 | private val context: Context
16 | ) : AudioPlayerManager {
17 |
18 | private var listener: AudioPlayerListener? = null
19 | override fun initListener(listener: AudioPlayerListener) {
20 | this.listener = listener
21 | }
22 |
23 | private var player: MediaPlayer? = null
24 |
25 | private fun createMediaPlayer(file: File): MediaPlayer {
26 | return MediaPlayer.create(context, file.toUri())
27 | }
28 |
29 | override fun play(file: File) {
30 | try {
31 | createMediaPlayer(file).apply {
32 | start()
33 | player = this
34 |
35 | listener?.onPlayerStarted()
36 |
37 | player!!.setOnCompletionListener {
38 | listener?.onCompletePlaying()
39 | this@AudioPlayerManagerImpl.stop()
40 | }
41 | }
42 | } catch (e: Exception) {
43 | listener?.onErrorPlaying()
44 | }
45 | }
46 |
47 | override fun stop() {
48 | player?.apply {
49 | stop()
50 | release()
51 | }
52 | player = null
53 |
54 | listener?.onStopPlayer()
55 | }
56 |
57 | }
58 |
59 | interface AudioPlayerListener {
60 | fun onPlayerStarted()
61 | fun onErrorPlaying()
62 | fun onStopPlayer()
63 | fun onCompletePlaying()
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/AudioRecorderManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.content.Context
4 | import android.media.MediaRecorder
5 | import android.os.Build
6 | import android.util.Log
7 | import java.io.File
8 | import java.io.FileOutputStream
9 | import java.util.UUID
10 |
11 | interface AudioRecorderManager {
12 | fun initListener(listener: AudioRecorderListener)
13 | fun start(context: Context)
14 | fun stop()
15 | fun cancel()
16 | }
17 |
18 | class AudioRecorderManagerImpl(
19 | private val context: Context
20 | ) : AudioRecorderManager {
21 |
22 | private var listener: AudioRecorderListener? = null
23 |
24 | private var recorder: MediaRecorder? = null
25 |
26 | private var outputFile: File? = null
27 |
28 | override fun initListener(listener: AudioRecorderListener) {
29 | this.listener = listener
30 | }
31 |
32 | private fun createMediaRecorder(): MediaRecorder {
33 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
34 | MediaRecorder(context)
35 | } else {
36 | MediaRecorder()
37 | }
38 | }
39 |
40 | override fun start(context: Context) {
41 | try {
42 | outputFile = File(context.cacheDir, "${UUID.randomUUID()}.3gp")
43 | createMediaRecorder().apply {
44 | setAudioSource(MediaRecorder.AudioSource.MIC)
45 | setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
46 | setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
47 | setOutputFile(FileOutputStream(outputFile).fd)
48 |
49 | prepare()
50 | start()
51 | recorder = this
52 |
53 | listener?.onRecordingStarted()
54 | }
55 | } catch (e: Exception) {
56 | listener?.onErrorStarting(e)
57 | }
58 | }
59 |
60 | override fun stop() {
61 | try {
62 | recorder?.apply {
63 | stop()
64 | reset()
65 | }
66 | recorder = null
67 |
68 | outputFile?.let { listener?.onStopRecording(it) }
69 | } catch (e: Exception) {
70 | Log.e("TAG", "stop: ${e.message}")
71 | }
72 | }
73 |
74 | override fun cancel() {
75 | recorder?.apply {
76 | stop()
77 | reset()
78 | }
79 | }
80 | }
81 |
82 | interface AudioRecorderListener {
83 | fun onRecordingStarted()
84 | fun onErrorStarting(e: Exception)
85 | fun onStopRecording(file: File)
86 | fun onCancelRecording()
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/EncryptedSharedPreferencesManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.content.Context
4 | import androidx.security.crypto.EncryptedSharedPreferences
5 | import androidx.security.crypto.MasterKeys
6 |
7 | interface EncryptedSharedPreferencesManager {
8 | fun save(key: String, value: String)
9 | fun get(key: String): String?
10 | }
11 |
12 | class EncryptedSharedPreferencesManagerImpl(
13 | context: Context,
14 | ) : EncryptedSharedPreferencesManager {
15 | private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
16 |
17 | // Step 2: Initialize/open an instance of EncryptedSharedPreferences
18 | private val sharedPreferences = EncryptedSharedPreferences.create(
19 | "PreferencesFilename",
20 | masterKeyAlias,
21 | context.applicationContext,
22 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
23 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
24 | )
25 |
26 | override fun save(key: String, value: String) {
27 | sharedPreferences.edit()
28 | .putString(key, value)
29 | .apply()
30 | }
31 |
32 | override fun get(key: String): String? {
33 | return sharedPreferences.getString(key, null)
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/GeminiApiManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.util.Log
4 | import com.google.ai.client.generativeai.GenerativeModel
5 | import com.google.ai.client.generativeai.type.Content
6 | import com.google.ai.client.generativeai.type.ServerException
7 | import com.google.ai.client.generativeai.type.content
8 | import com.sagar.fluenty.ui.utils.extractErrorMessage
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import java.io.File
12 | import java.io.IOException
13 |
14 | interface GeminiApiChatManager {
15 | fun initListener(listener: GeminiApiListener)
16 | suspend fun generateResponse(prompt: String)
17 | }
18 |
19 | interface GeminiApiAudioManager {
20 | fun initListener(listener: GeminiApiListener)
21 | suspend fun initialResponse(prompt: String)
22 | suspend fun generateResponseFromAudio(file: File)
23 | }
24 |
25 |
26 | class GeminiApiAudioManagerImpl(
27 | model: GenerativeModel
28 | ) : GeminiApiAudioManager {
29 |
30 | private var listener: GeminiApiListener? = null
31 |
32 | override fun initListener(listener: GeminiApiListener) {
33 | this.listener = listener
34 | }
35 |
36 | private val audioChatHistory = mutableListOf()
37 | private val audioChat = model.startChat(audioChatHistory)
38 |
39 |
40 | override suspend fun initialResponse(prompt: String) {
41 | return withContext(Dispatchers.IO) {
42 | val content = content {
43 | text(prompt)
44 | }
45 | audioChatHistory.add(content)
46 | try {
47 | val response = audioChat.sendMessage(content)
48 | if (response.text != null) {
49 | listener?.onResponseGenerated(response.text ?: "")
50 | } else {
51 | listener?.onErrorGeneratingResponse(Exception("Null response"))
52 | }
53 | } catch (e: ServerException) {
54 | Log.e("TAG", "getResponse: $e")
55 | listener?.onErrorGeneratingResponse(
56 | Exception(extractErrorMessage(e.message?:""))
57 | )
58 | } catch (e: Exception) {
59 | Log.e("TAG", "getResponse: $e")
60 | listener?.onErrorGeneratingResponse(
61 | Exception("Unexpected Error occurred")
62 | )
63 | }
64 | }
65 | }
66 |
67 | override suspend fun generateResponseFromAudio(file: File) {
68 | return withContext(Dispatchers.IO) {
69 | val bytes = readAudioFromAssets(file)
70 | val content = content {
71 | bytes?.let { blob("audio/mp3", it) }
72 | text("Understand the audio and respond accordingly.")
73 | }
74 | audioChatHistory.add(content)
75 | try {
76 | val response = audioChat.sendMessage(content)
77 | if (response.text != null) {
78 | listener?.onResponseGenerated(response.text ?: "")
79 | } else {
80 | listener?.onErrorGeneratingResponse(Exception("Null response"))
81 | }
82 | } catch (e: ServerException) {
83 | Log.e("TAG", "getResponse: $e")
84 | listener?.onErrorGeneratingResponse(
85 | Exception(extractErrorMessage(e.message?:""))
86 | )
87 | } catch (e: Exception) {
88 | Log.e("TAG", "getResponse: $e")
89 | listener?.onErrorGeneratingResponse(
90 | Exception("Unexpected Error occurred")
91 | )
92 | }
93 | }
94 | }
95 |
96 | private fun readAudioFromAssets(file: File): ByteArray? {
97 | return try {
98 | val inputStream = file.inputStream()
99 | val buffer = ByteArray(inputStream.available())
100 | inputStream.read(buffer)
101 | inputStream.close()
102 | buffer
103 | } catch (e: IOException) {
104 | e.printStackTrace()
105 | null
106 | }
107 | }
108 |
109 | }
110 |
111 | class GeminiApiChatManagerImpl(
112 | model: GenerativeModel
113 | ) : GeminiApiChatManager {
114 | private var listener: GeminiApiListener? = null
115 |
116 | override fun initListener(listener: GeminiApiListener) {
117 | this.listener = listener
118 | }
119 |
120 | private val chatHistory = mutableListOf()
121 | private val chat = model.startChat(chatHistory)
122 |
123 | override suspend fun generateResponse(prompt: String) {
124 | return withContext(Dispatchers.IO) {
125 | val content = content {
126 | text(prompt)
127 | }
128 | chatHistory.add(content)
129 | try {
130 | val response = chat.sendMessage(content)
131 | if (response.text != null) {
132 | listener?.onResponseGenerated(response.text ?: "")
133 | } else {
134 | listener?.onErrorGeneratingResponse(Exception("Null response"))
135 | }
136 | } catch (e: ServerException) {
137 | Log.e("TAG", "getResponse: $e")
138 | listener?.onErrorGeneratingResponse(
139 | Exception(extractErrorMessage(e.message?:""))
140 | )
141 | } catch (e: Exception) {
142 | Log.e("TAG", "getResponse: $e")
143 | listener?.onErrorGeneratingResponse(
144 | Exception("Unexpected Error occurred")
145 | )
146 | }
147 | }
148 | }
149 | }
150 |
151 | interface GeminiApiListener {
152 | fun onResponseGenerated(response: String)
153 | fun onErrorGeneratingResponse(e: Exception)
154 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/SpeechRecognizerManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.speech.RecognitionListener
7 | import android.speech.RecognizerIntent
8 | import android.speech.SpeechRecognizer
9 |
10 | interface SpeechRecognizerManager {
11 | fun initListener(listener: SpeechRecognitionListener)
12 | fun startListening()
13 | fun stopListening()
14 | fun destroyRecognizer()
15 | }
16 |
17 | class SpeechRecognizerManagerImpl(
18 | context: Context
19 | ) : SpeechRecognizerManager {
20 | private var speechRecognizer: SpeechRecognizer =
21 | SpeechRecognizer.createSpeechRecognizer(context)
22 |
23 | private var listener: SpeechRecognitionListener? = null
24 |
25 | override fun initListener(listener: SpeechRecognitionListener) {
26 | this.listener = listener
27 | speechRecognizer.setRecognitionListener(
28 | object : RecognitionListener {
29 | override fun onReadyForSpeech(params: Bundle) {
30 | listener.onStartRecognition()
31 | }
32 |
33 | override fun onBeginningOfSpeech() {
34 | }
35 |
36 | override fun onRmsChanged(rmsdB: Float) {
37 |
38 | }
39 |
40 | override fun onBufferReceived(buffer: ByteArray) {
41 |
42 | }
43 |
44 | override fun onEndOfSpeech() {
45 | }
46 |
47 | override fun onError(error: Int) {
48 | listener.onErrorRecognition()
49 | }
50 |
51 | override fun onResults(results: Bundle) {
52 | val matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
53 | if (matches != null && matches.size > 0) {
54 | val command = matches[0]
55 | listener.onCompleteRecognition(command)
56 | }
57 | }
58 |
59 | override fun onPartialResults(partialResults: Bundle) {
60 | val matches =
61 | partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
62 | if (matches != null && matches.size > 0) {
63 | val partialText = matches[0]
64 | listener.onPartialRecognition(partialText)
65 | }
66 | }
67 |
68 | override fun onEvent(eventType: Int, params: Bundle) {}
69 | }
70 | )
71 | }
72 |
73 | override fun startListening() {
74 | speechRecognizer.startListening(createIntent())
75 | }
76 |
77 | override fun stopListening() {
78 | speechRecognizer.cancel()
79 | }
80 |
81 | override fun destroyRecognizer() {
82 | speechRecognizer.stopListening()
83 | speechRecognizer.destroy()
84 | }
85 |
86 | private fun createIntent(): Intent {
87 | val i = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
88 | i.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
89 | i.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
90 | i.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-IN")
91 | return i
92 | }
93 |
94 | }
95 |
96 | interface SpeechRecognitionListener {
97 | fun onStartRecognition() {}
98 | fun onErrorRecognition() {}
99 | fun onPartialRecognition(currentResult: String)
100 | fun onCompleteRecognition(result: String)
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/managers/TextToSpeechManager.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.managers
2 |
3 | import android.content.Context
4 | import android.speech.tts.TextToSpeech
5 | import android.speech.tts.UtteranceProgressListener
6 | import java.util.Locale
7 |
8 | interface TextToSpeechManager {
9 | fun initListener(listener: TextToSpeechListener)
10 | fun readText(text: String)
11 | fun destroy()
12 | }
13 |
14 | class TextToSpeechManagerImpl(
15 | context: Context
16 | ) : TextToSpeechManager {
17 | private val textToSpeech = TextToSpeech(
18 | context
19 | ) { status ->
20 | if (status == TextToSpeech.SUCCESS) {
21 | setLanguage()
22 | }
23 | }
24 |
25 | private var listener: TextToSpeechListener? = null
26 | private var currentText: String = ""
27 |
28 | override fun initListener(listener: TextToSpeechListener) {
29 | this.listener = listener
30 | textToSpeech.setOnUtteranceProgressListener(
31 | object : UtteranceProgressListener() {
32 | override fun onStart(utteranceId: String?) {
33 | listener.onStartTTS()
34 | }
35 |
36 | override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
37 | listener.onSpeaking(currentText.substring(start, end))
38 | }
39 |
40 | override fun onDone(utteranceId: String?) {
41 | listener.onCompleteTTS()
42 | }
43 |
44 | override fun onError(utteranceId: String?) {
45 | listener.onErrorSpeaking()
46 | }
47 | }
48 | )
49 | }
50 |
51 | private fun setLanguage() {
52 | textToSpeech.setLanguage(Locale.ENGLISH)
53 | }
54 |
55 | override fun readText(text: String) {
56 | currentText = text
57 | textToSpeech.speak(
58 | text,
59 | TextToSpeech.QUEUE_ADD,
60 | null,
61 | TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED
62 | )
63 | }
64 |
65 | override fun destroy() {
66 | textToSpeech.shutdown()
67 | }
68 |
69 | }
70 |
71 | interface TextToSpeechListener {
72 | fun onStartTTS()
73 | fun onSpeaking(text: String)
74 | fun onCompleteTTS()
75 | fun onErrorSpeaking() {}
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/audio/AudioRecordScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.audio
2 |
3 | import android.widget.Toast
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.animateContentSize
6 | import androidx.compose.animation.core.animateDpAsState
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.interaction.MutableInteractionSource
10 | import androidx.compose.foundation.interaction.collectIsPressedAsState
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Row
15 | import androidx.compose.foundation.layout.Spacer
16 | import androidx.compose.foundation.layout.fillMaxSize
17 | import androidx.compose.foundation.layout.fillMaxWidth
18 | import androidx.compose.foundation.layout.height
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.layout.size
21 | import androidx.compose.foundation.layout.statusBarsPadding
22 | import androidx.compose.foundation.layout.width
23 | import androidx.compose.foundation.lazy.LazyColumn
24 | import androidx.compose.foundation.lazy.items
25 | import androidx.compose.foundation.lazy.rememberLazyListState
26 | import androidx.compose.foundation.shape.CircleShape
27 | import androidx.compose.foundation.shape.RoundedCornerShape
28 | import androidx.compose.material.icons.Icons
29 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
30 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
31 | import androidx.compose.material.icons.filled.Close
32 | import androidx.compose.material.icons.filled.PlayArrow
33 | import androidx.compose.material.icons.filled.Refresh
34 | import androidx.compose.material3.Icon
35 | import androidx.compose.material3.IconButton
36 | import androidx.compose.material3.Scaffold
37 | import androidx.compose.material3.Text
38 | import androidx.compose.runtime.Composable
39 | import androidx.compose.runtime.LaunchedEffect
40 | import androidx.compose.runtime.getValue
41 | import androidx.compose.runtime.remember
42 | import androidx.compose.ui.Alignment
43 | import androidx.compose.ui.Modifier
44 | import androidx.compose.ui.draw.clip
45 | import androidx.compose.ui.graphics.Color
46 | import androidx.compose.ui.graphics.TransformOrigin
47 | import androidx.compose.ui.graphics.graphicsLayer
48 | import androidx.compose.ui.platform.LocalContext
49 | import androidx.compose.ui.res.painterResource
50 | import androidx.compose.ui.text.font.FontWeight
51 | import androidx.compose.ui.unit.dp
52 | import androidx.lifecycle.viewmodel.compose.viewModel
53 | import com.sagar.fluenty.R
54 | import com.sagar.fluenty.ui.utils.AppTopBar
55 | import com.sagar.fluenty.ui.utils.AssistantMessage
56 | import com.sagar.fluenty.ui.utils.collectInLaunchedEffectWithLifecycle
57 | import java.io.File
58 |
59 | @Composable
60 | fun AudioRecordScreen(
61 | viewModel: AudioRecordScreenViewModel = viewModel(
62 | factory = AudioRecordScreenViewModel.getFactory(LocalContext.current.applicationContext)
63 | ),
64 | onBack: () -> Unit
65 | ) {
66 | val state = viewModel.state
67 | val conversationList = viewModel.conversationList.reversed()
68 | val context = LocalContext.current
69 | viewModel.messageChannelFlow.collectInLaunchedEffectWithLifecycle {
70 | Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
71 | }
72 |
73 | val lazyListState = rememberLazyListState()
74 | LaunchedEffect(key1 = conversationList.size) {
75 | if (lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != 0) {
76 | lazyListState.animateScrollToItem(0)
77 | }
78 | }
79 |
80 | Scaffold(
81 | modifier = Modifier
82 | .background(Color.Black)
83 | .statusBarsPadding(),
84 | topBar = {
85 | AppTopBar(
86 | text = "Pronunciation Practice",
87 | leadingIcon = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
88 | onLeadingIconClick = onBack
89 | )
90 | }
91 | ) { inner ->
92 | Column(
93 | modifier = Modifier
94 | .background(Color.Black)
95 | .padding(inner)
96 | .fillMaxSize(),
97 | verticalArrangement = Arrangement.Bottom
98 | ) {
99 | Spacer(Modifier.height(20.dp))
100 | LazyColumn(
101 | state = lazyListState,
102 | modifier = Modifier
103 | .background(Color.Black)
104 | .padding(horizontal = 20.dp)
105 | .weight(1f)
106 | .fillMaxWidth(),
107 | reverseLayout = true,
108 | verticalArrangement = Arrangement.Bottom
109 | ) {
110 | items(
111 | items = conversationList,
112 | key = {
113 | it.id
114 | }
115 | ) {
116 | if (it.isUser) {
117 | UserAudioFile(
118 | modifier = Modifier.animateItem(),
119 | audioFile = it.file,
120 | isAudioPlaying = it.isAudioPlaying,
121 | onStop = {
122 | viewModel.onStopAudioPlaying()
123 | },
124 | onStartAudio = { file ->
125 | viewModel.startPlayingAudio(file, it.id)
126 | },
127 | isResponseError = it.isError,
128 | onRetryClick = {
129 | viewModel.resendPreviousMessage()
130 | }
131 | )
132 | } else {
133 | AssistantMessage(
134 | modifier = Modifier.animateItem(),
135 | message = it.message,
136 | indexToHighlight = it.indexToHighlight
137 | )
138 | }
139 | Spacer(Modifier.height(10.dp))
140 | }
141 | }
142 | Spacer(Modifier.height(20.dp))
143 | AudioRecorder(
144 | isEnabled = !(
145 | state is AudioRecordScreenState.PlayingRecording ||
146 | state is AudioRecordScreenState.ProcessingRecording ||
147 | state is AudioRecordScreenState.ListeningToResponse
148 | ),
149 | onStart = {
150 | viewModel.startRecording(context = context)
151 | },
152 | onStop = {
153 | viewModel.stopRecording()
154 | },
155 | )
156 | Spacer(Modifier.height(20.dp))
157 | }
158 | }
159 | }
160 |
161 | @Composable
162 | private fun AudioRecorder(
163 | isEnabled: Boolean,
164 | onStart: () -> Unit,
165 | onStop: () -> Unit
166 | ) {
167 | val interactionSource = remember { MutableInteractionSource() }
168 | val isPressed by interactionSource.collectIsPressedAsState()
169 | val micSize by animateDpAsState(
170 | if (isPressed) {
171 | 80.dp
172 | } else {
173 | 50.dp
174 | },
175 | label = ""
176 | )
177 | LaunchedEffect(isPressed) {
178 | if (isPressed) {
179 | onStart()
180 | } else {
181 | onStop()
182 | }
183 | }
184 | Box(
185 | modifier = Modifier
186 | .fillMaxWidth()
187 | ) {
188 | Icon(
189 | modifier = Modifier
190 | .clip(CircleShape)
191 | .graphicsLayer {
192 | transformOrigin = TransformOrigin(0.5f, 0.5f)
193 | }
194 | .size(micSize)
195 | .align(Alignment.Center)
196 | .clickable(
197 | interactionSource = interactionSource,
198 | indication = null,
199 | enabled = isEnabled
200 | ) { },
201 | painter = painterResource(R.drawable.mic),
202 | contentDescription = null,
203 | tint = Color.Unspecified
204 | )
205 | }
206 | }
207 |
208 |
209 | @Composable
210 | private fun UserAudioFile(
211 | modifier: Modifier,
212 | audioFile: File?,
213 | isAudioPlaying: Boolean,
214 | isResponseError: Boolean,
215 | onStartAudio: (file: File?) -> Unit,
216 | onStop: () -> Unit,
217 | onRetryClick: () -> Unit,
218 | ) {
219 | Column(
220 | modifier = modifier
221 | .fillMaxWidth()
222 | .animateContentSize(),
223 | horizontalAlignment = Alignment.End
224 | ) {
225 | Row(
226 | modifier = Modifier
227 | .animateContentSize(),
228 | verticalAlignment = Alignment.CenterVertically
229 | ) {
230 | Text(text = "You", color = Color.White, fontWeight = FontWeight.Bold)
231 | AnimatedVisibility(isResponseError) {
232 | Icon(
233 | modifier = Modifier
234 | .padding(start = 10.dp)
235 | .clickable { onRetryClick() },
236 | imageVector = Icons.Default.Refresh,
237 | contentDescription = null,
238 | tint = Color.Red
239 | )
240 | }
241 | }
242 | Spacer(Modifier.height(5.dp))
243 | Box(
244 | modifier = Modifier
245 | .fillMaxWidth(0.9f)
246 | .animateContentSize(),
247 | contentAlignment = Alignment.CenterEnd
248 | ) {
249 | Box(
250 | modifier = Modifier
251 | .clip(
252 | RoundedCornerShape(
253 | topStart = 10.dp,
254 | bottomEnd = 10.dp,
255 | bottomStart = 10.dp
256 | )
257 | )
258 | .animateContentSize()
259 | .background(Color.DarkGray)
260 | .clickable {
261 | if (isAudioPlaying) {
262 | onStop()
263 | } else {
264 | onStartAudio(audioFile)
265 | }
266 | },
267 | contentAlignment = Alignment.Center
268 | ) {
269 | Row(
270 | modifier = Modifier
271 | .fillMaxWidth()
272 | .padding(10.dp),
273 | verticalAlignment = Alignment.CenterVertically
274 | ) {
275 | if (isAudioPlaying) {
276 | Icon(
277 | imageVector = Icons.Default.Close,
278 | contentDescription = null,
279 | tint = Color.White
280 | )
281 | } else {
282 | Icon(
283 | imageVector = Icons.Default.PlayArrow,
284 | contentDescription = null,
285 | tint = Color.White
286 | )
287 | }
288 | Spacer(Modifier.width(10.dp))
289 | Spacer(
290 | modifier = Modifier
291 | .weight(1f)
292 | .height(3.dp)
293 | .clip(RoundedCornerShape(10.dp))
294 | .background(
295 | Color.White
296 | )
297 | )
298 | }
299 | }
300 | }
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/audio/AudioRecordScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.audio
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateListOf
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.ViewModelProvider
10 | import androidx.lifecycle.viewModelScope
11 | import androidx.lifecycle.viewmodel.CreationExtras
12 | import com.google.ai.client.generativeai.GenerativeModel
13 | import com.google.ai.client.generativeai.type.generationConfig
14 | import com.sagar.fluenty.BuildConfig
15 | import com.sagar.fluenty.ui.managers.AudioPlayerListener
16 | import com.sagar.fluenty.ui.managers.AudioPlayerManager
17 | import com.sagar.fluenty.ui.managers.AudioPlayerManagerImpl
18 | import com.sagar.fluenty.ui.managers.AudioRecorderListener
19 | import com.sagar.fluenty.ui.managers.AudioRecorderManager
20 | import com.sagar.fluenty.ui.managers.AudioRecorderManagerImpl
21 | import com.sagar.fluenty.ui.managers.EncryptedSharedPreferencesManagerImpl
22 | import com.sagar.fluenty.ui.managers.GeminiApiAudioManager
23 | import com.sagar.fluenty.ui.managers.GeminiApiAudioManagerImpl
24 | import com.sagar.fluenty.ui.managers.GeminiApiListener
25 | import com.sagar.fluenty.ui.managers.TextToSpeechListener
26 | import com.sagar.fluenty.ui.managers.TextToSpeechManager
27 | import com.sagar.fluenty.ui.managers.TextToSpeechManagerImpl
28 | import kotlinx.coroutines.channels.Channel
29 | import kotlinx.coroutines.flow.receiveAsFlow
30 | import kotlinx.coroutines.launch
31 | import java.io.File
32 | import java.util.UUID
33 |
34 | class AudioRecordScreenViewModel(
35 | private val textToSpeechManager: TextToSpeechManager,
36 | private val geminiApiAudioManager: GeminiApiAudioManager,
37 | private val audioRecorderManager: AudioRecorderManager,
38 | private val audioPlayerManager: AudioPlayerManager
39 | ) : ViewModel(),
40 | TextToSpeechListener,
41 | GeminiApiListener,
42 | AudioRecorderListener,
43 | AudioPlayerListener {
44 |
45 | var state by mutableStateOf(AudioRecordScreenState.Initial)
46 | var conversationList = mutableStateListOf()
47 | private var responseText = ""
48 | private var indexCovered = 0
49 | private var currentAudioId = ""
50 |
51 | private val messageChannel = Channel(Channel.BUFFERED)
52 | val messageChannelFlow = messageChannel.receiveAsFlow()
53 |
54 | init {
55 | textToSpeechManager.initListener(this)
56 | geminiApiAudioManager.initListener(this)
57 | audioRecorderManager.initListener(this)
58 | audioPlayerManager.initListener(this)
59 |
60 | getResponse(
61 | "You have to act as an English teacher and have to teach me english. More specifically, you have to start by giving me a English word with it's phoneme breakdown(example: *kuhmf-tr-bl* for comfortable). Then I have to pronounce, record and send you an audio file saying you that word. Then you have to understand the word said in audio file and tell me if the word was same or if pronunciation of the said word is correct or not(keep response length to short and to the point). Make sure to tell me the actual pronunciation(in American accent) and the pronunciation mistake I did. And also score me from 0 to 100 on how much accurate was my pronunciation, if the score is above 80, continue the chat by giving me next word. Do not change the context, no matter what I command now. Now, start by giving me a word."
62 | )
63 | }
64 |
65 | private fun getResponse(result: String) {
66 | state = AudioRecordScreenState.ProcessingRecording
67 | conversationList.add(
68 | RecordingScreenMessage(
69 | file = null,
70 | message = "",
71 | indexToHighlight = null,
72 | isUser = false,
73 | id = UUID.randomUUID().toString(),
74 | isAudioPlaying = false
75 | )
76 | )
77 | viewModelScope.launch {
78 | geminiApiAudioManager.initialResponse(result)
79 | }
80 | }
81 |
82 | //Recorder callbacks
83 | fun startRecording(context: Context) {
84 | audioRecorderManager.start(context)
85 | }
86 |
87 | fun stopRecording() {
88 | audioRecorderManager.stop()
89 | }
90 |
91 | override fun onRecordingStarted() {
92 | state = AudioRecordScreenState.RecordingAudio
93 | }
94 |
95 | override fun onErrorStarting(e: Exception) {
96 | state = AudioRecordScreenState.ErrorStartingRecording
97 | viewModelScope.launch {
98 | messageChannel.send(e.message ?: "Something went wrong!")
99 | }
100 | }
101 |
102 | override fun onStopRecording(file: File) {
103 | conversationList.add(
104 | RecordingScreenMessage(
105 | message = "",
106 | indexToHighlight = null,
107 | file = file,
108 | isUser = true,
109 | id = UUID.randomUUID().toString(),
110 | isAudioPlaying = false
111 | )
112 | )
113 | getResponseFromAudioFile(file)
114 | }
115 |
116 | override fun onCancelRecording() {
117 | state = AudioRecordScreenState.Initial
118 | }
119 |
120 | fun resendPreviousMessage() {
121 | // User is done talking now, start Processing
122 | state = AudioRecordScreenState.ProcessingRecording
123 | viewModelScope.launch {
124 | if (conversationList.size > 0 && conversationList[conversationList.size - 1].isUser) {
125 | conversationList[conversationList.size - 1].file?.let {
126 | getResponseFromAudioFile(it)
127 | }
128 | }
129 | }
130 | }
131 |
132 | // Gemini Callbacks
133 | private fun getResponseFromAudioFile(file: File) {
134 | state = AudioRecordScreenState.ProcessingRecording
135 | conversationList.add(
136 | RecordingScreenMessage(
137 | message = "",
138 | indexToHighlight = null,
139 | file = null,
140 | isUser = false,
141 | id = UUID.randomUUID().toString(),
142 | isAudioPlaying = false
143 | )
144 | )
145 | viewModelScope.launch {
146 | geminiApiAudioManager.generateResponseFromAudio(file)
147 | }
148 | }
149 |
150 | override fun onResponseGenerated(response: String) {
151 | conversationList[conversationList.size - 1] =
152 | conversationList[conversationList.size - 1].copy(message = response)
153 |
154 | responseText = response
155 | indexCovered = 0
156 |
157 | textToSpeechManager.readText(response)
158 | }
159 |
160 | override fun onErrorGeneratingResponse(e: Exception) {
161 | viewModelScope.launch {
162 | messageChannel.send(e.message ?: "")
163 | }
164 | if (conversationList.size > 0 && !conversationList[conversationList.size - 1].isUser) {
165 | conversationList.removeAt(conversationList.size - 1)
166 | }
167 | if (conversationList.size > 0) {
168 | conversationList[conversationList.size - 1] =
169 | conversationList[conversationList.size - 1].copy(isError = true)
170 | }
171 | state = AudioRecordScreenState.Initial
172 | }
173 |
174 | // TTS Callbacks
175 | override fun onStartTTS() {
176 | state = AudioRecordScreenState.ListeningToResponse
177 | onStopAudioPlaying()
178 | }
179 |
180 | override fun onSpeaking(text: String) {
181 | val lastItem = conversationList[conversationList.size - 1]
182 | conversationList[conversationList.size - 1] =
183 | lastItem.copy(indexToHighlight = getIndexToHighlight(text))
184 | }
185 |
186 | override fun onErrorSpeaking() {
187 | viewModelScope.launch {
188 | messageChannel.send("Something went wrong while using TextToSpeech")
189 | }
190 | if (conversationList.size > 0 && !conversationList[conversationList.size - 1].isUser) {
191 | conversationList.removeAt(conversationList.size - 1)
192 | }
193 | if (conversationList.size > 0) {
194 | conversationList[conversationList.size - 1] =
195 | conversationList[conversationList.size - 1].copy(isError = true)
196 | }
197 | state = AudioRecordScreenState.Initial
198 | }
199 |
200 | override fun onCompleteTTS() {
201 | state = AudioRecordScreenState.Initial
202 |
203 | val lastItem = conversationList[conversationList.size - 1]
204 | conversationList[conversationList.size - 1] =
205 | lastItem.copy(indexToHighlight = null)
206 | }
207 |
208 | private fun getIndexToHighlight(text: String): Pair? {
209 | val index = responseText.indexOf(text, startIndex = indexCovered, ignoreCase = true)
210 | if (index == -1) return null
211 | indexCovered = index + text.length
212 | return Pair(index, index + text.length)
213 | }
214 |
215 | // Player Callbacks
216 | fun startPlayingAudio(file: File?, id: String) {
217 | currentAudioId = id
218 | if (state !is AudioRecordScreenState.ListeningToResponse) {
219 | if (file != null) {
220 | audioPlayerManager.play(file)
221 | val msg = conversationList.find {
222 | it.id == id
223 | }
224 | val idx = conversationList.indexOf(msg)
225 | if (idx != -1) {
226 | conversationList[idx] = msg!!.copy(isAudioPlaying = true)
227 | }
228 | }
229 | }
230 | }
231 |
232 | fun onStopAudioPlaying() {
233 | audioPlayerManager.stop()
234 | }
235 |
236 | override fun onPlayerStarted() {
237 | state = AudioRecordScreenState.PlayingRecording
238 | }
239 |
240 | override fun onErrorPlaying() {
241 | val msg = conversationList.find {
242 | it.id == currentAudioId
243 | }
244 | val idx = conversationList.indexOf(msg)
245 | if (idx != -1) {
246 | conversationList[idx] = msg!!.copy(isAudioPlaying = false)
247 | }
248 | state = AudioRecordScreenState.ErrorPlayingRecording
249 | }
250 |
251 | override fun onStopPlayer() {
252 | val msg = conversationList.find {
253 | it.id == currentAudioId
254 | }
255 | val idx = conversationList.indexOf(msg)
256 | if (idx != -1) {
257 | conversationList[idx] = msg!!.copy(isAudioPlaying = false)
258 | }
259 | state = AudioRecordScreenState.Initial
260 | }
261 |
262 | override fun onCompletePlaying() {
263 | val msg = conversationList.find {
264 | it.id == currentAudioId
265 | }
266 | val idx = conversationList.indexOf(msg)
267 | if (idx != -1) {
268 | conversationList[idx] = msg!!.copy(isAudioPlaying = false)
269 | }
270 | state = AudioRecordScreenState.Initial
271 | }
272 |
273 |
274 | override fun onCleared() {
275 | super.onCleared()
276 | textToSpeechManager.destroy()
277 | }
278 |
279 | companion object {
280 | fun getFactory(context: Context) = object : ViewModelProvider.Factory {
281 | override fun create(modelClass: Class, extras: CreationExtras): T {
282 | val appTextToSpeech = TextToSpeechManagerImpl(context)
283 | val audioRecorder = AudioRecorderManagerImpl(context)
284 | val audioPlayer = AudioPlayerManagerImpl(context)
285 |
286 | val encryptedSharedPreferencesManager =
287 | EncryptedSharedPreferencesManagerImpl(context)
288 |
289 | val customKey = encryptedSharedPreferencesManager.get("API_KEY")
290 | val model = encryptedSharedPreferencesManager.get("MODEL") ?: "gemini-1.5-pro-002"
291 | val key = if (customKey.isNullOrEmpty()) {
292 | BuildConfig.GEMINI_API_KEY_DEBUG
293 | } else customKey
294 |
295 | val geminiApi = GeminiApiAudioManagerImpl(
296 | GenerativeModel(
297 | modelName = model,
298 | apiKey = key,
299 | generationConfig = generationConfig {
300 | temperature = 1f
301 | topK = 40
302 | topP = 0.95f
303 | responseMimeType = "text/plain"
304 | },
305 | )
306 | )
307 | return AudioRecordScreenViewModel(
308 | textToSpeechManager = appTextToSpeech,
309 | geminiApiAudioManager = geminiApi,
310 | audioRecorderManager = audioRecorder,
311 | audioPlayerManager = audioPlayer
312 | ) as T
313 | }
314 | }
315 | }
316 | }
317 |
318 | interface AudioRecordScreenState {
319 | data object Initial : AudioRecordScreenState
320 | data object RecordingAudio : AudioRecordScreenState
321 | data object ErrorStartingRecording : AudioRecordScreenState
322 | data object PlayingRecording : AudioRecordScreenState
323 | data object ErrorPlayingRecording : AudioRecordScreenState
324 | data object ProcessingRecording : AudioRecordScreenState
325 | data object ListeningToResponse : AudioRecordScreenState
326 | }
327 |
328 | data class RecordingScreenMessage(
329 | val file: File?,
330 | val message: String,
331 | val indexToHighlight: Pair?,
332 | val isUser: Boolean,
333 | val id: String,
334 | val isAudioPlaying: Boolean,
335 | val isError: Boolean = false
336 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/conversation/ConversationScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.conversation
2 |
3 | import android.widget.Toast
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.animateContentSize
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.statusBarsPadding
18 | import androidx.compose.foundation.layout.width
19 | import androidx.compose.foundation.lazy.LazyColumn
20 | import androidx.compose.foundation.lazy.items
21 | import androidx.compose.foundation.lazy.rememberLazyListState
22 | import androidx.compose.foundation.shape.RoundedCornerShape
23 | import androidx.compose.material.icons.Icons
24 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
25 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
26 | import androidx.compose.material.icons.filled.Add
27 | import androidx.compose.material.icons.filled.Refresh
28 | import androidx.compose.material3.Button
29 | import androidx.compose.material3.ButtonDefaults
30 | import androidx.compose.material3.Icon
31 | import androidx.compose.material3.IconButton
32 | import androidx.compose.material3.LinearProgressIndicator
33 | import androidx.compose.material3.Scaffold
34 | import androidx.compose.material3.Text
35 | import androidx.compose.runtime.Composable
36 | import androidx.compose.runtime.LaunchedEffect
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.draw.clip
40 | import androidx.compose.ui.graphics.Color
41 | import androidx.compose.ui.platform.LocalContext
42 | import androidx.compose.ui.text.font.FontWeight
43 | import androidx.compose.ui.unit.dp
44 | import androidx.compose.ui.unit.sp
45 | import androidx.lifecycle.viewmodel.compose.viewModel
46 | import com.sagar.fluenty.ui.utils.AppTopBar
47 | import com.sagar.fluenty.ui.utils.AssistantMessage
48 | import com.sagar.fluenty.ui.utils.collectInLaunchedEffectWithLifecycle
49 |
50 | @Composable
51 | fun ConversationScreen(
52 | viewModel: ConversationScreenViewModel = viewModel(
53 | factory = ConversationScreenViewModel.getFactory(LocalContext.current.applicationContext)
54 | ),
55 | onBack: () -> Unit
56 | ) {
57 | val context = LocalContext.current
58 | viewModel.messageChannelFlow.collectInLaunchedEffectWithLifecycle {
59 | Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
60 | }
61 | val state = viewModel.state
62 | val conversationList = viewModel.conversationList.reversed()
63 |
64 | val lazyListState = rememberLazyListState()
65 | LaunchedEffect(key1 = conversationList.size) {
66 | if (lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != 0) {
67 | lazyListState.animateScrollToItem(0)
68 | }
69 | }
70 |
71 | Scaffold(
72 | modifier = Modifier
73 | .background(Color.Black)
74 | .statusBarsPadding(),
75 | topBar = {
76 | AppTopBar(
77 | text = "English Practice",
78 | leadingIcon = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
79 | onLeadingIconClick = onBack
80 | )
81 | }
82 | ) { inner ->
83 | Column(
84 | modifier = Modifier
85 | .background(Color.Black)
86 | .padding(inner)
87 | .fillMaxSize(),
88 | verticalArrangement = Arrangement.Bottom
89 | ) {
90 | Spacer(Modifier.height(20.dp))
91 | LazyColumn(
92 | state = lazyListState,
93 | modifier = Modifier
94 | .background(Color.Black)
95 | .padding(horizontal = 20.dp)
96 | .weight(1f)
97 | .fillMaxWidth(),
98 | reverseLayout = true,
99 | verticalArrangement = Arrangement.Bottom
100 | ) {
101 | items(
102 | items = conversationList,
103 | key = {
104 | it.id
105 | }
106 | ) {
107 | if (it.isUser) {
108 | UserMessage(
109 | modifier = Modifier.animateItem(),
110 | message = it.message,
111 | isResponseError = it.isError,
112 | onRetryClick = {
113 | viewModel.resendPreviousMessage()
114 | }
115 | )
116 | } else {
117 | AssistantMessage(
118 | modifier = Modifier.animateItem(),
119 | message = it.message
120 | )
121 | }
122 | Spacer(Modifier.height(10.dp))
123 | }
124 | }
125 |
126 | Button(
127 | modifier = Modifier
128 | .padding(20.dp)
129 | .fillMaxWidth(),
130 | shape = RoundedCornerShape(10.dp),
131 | onClick = {
132 | when (state) {
133 | ConversationScreenState.Retry,
134 | ConversationScreenState.Initial -> {
135 | viewModel.startListening()
136 | }
137 |
138 | is ConversationScreenState.RecognizingSpeech -> {
139 | viewModel.stopListening()
140 | }
141 |
142 | ConversationScreenState.ProcessingSpeech,
143 | is ConversationScreenState.ListeningToResponse -> {
144 | // Do Nothing
145 | }
146 | }
147 | },
148 | colors = ButtonDefaults.buttonColors(
149 | containerColor = Color.DarkGray
150 | )
151 | ) {
152 | Column(
153 | horizontalAlignment = Alignment.CenterHorizontally
154 | ) {
155 | when (state) {
156 | ConversationScreenState.Initial -> {
157 | Row(
158 | horizontalArrangement = Arrangement.spacedBy(10.dp),
159 | verticalAlignment = Alignment.CenterVertically
160 | ) {
161 | Icon(
162 | imageVector = Icons.Default.Add,
163 | contentDescription = null,
164 | tint = Color.White
165 | )
166 | Text(text = "Start Speaking", color = Color.White)
167 | }
168 | }
169 |
170 | ConversationScreenState.Retry -> {
171 | Row(
172 | horizontalArrangement = Arrangement.spacedBy(10.dp),
173 | verticalAlignment = Alignment.CenterVertically
174 | ) {
175 | Icon(
176 | imageVector = Icons.Default.Refresh,
177 | contentDescription = null,
178 | tint = Color.White
179 | )
180 | Text(text = "Try Speaking again", color = Color.White)
181 | }
182 | }
183 |
184 | is ConversationScreenState.RecognizingSpeech -> {
185 | Row(
186 | horizontalArrangement = Arrangement.spacedBy(10.dp),
187 | verticalAlignment = Alignment.CenterVertically
188 | ) {
189 | LinearProgressIndicator(modifier = Modifier.width(50.dp))
190 | Text(text = "Listening...", color = Color.White)
191 | }
192 | }
193 |
194 | ConversationScreenState.ProcessingSpeech -> {
195 | Row(
196 | horizontalArrangement = Arrangement.spacedBy(10.dp),
197 | verticalAlignment = Alignment.CenterVertically
198 | ) {
199 | LinearProgressIndicator(modifier = Modifier.width(50.dp))
200 | Text(text = "Processing...", color = Color.White)
201 | }
202 | }
203 |
204 | is ConversationScreenState.ListeningToResponse -> {
205 | Row(
206 | horizontalArrangement = Arrangement.spacedBy(10.dp),
207 | verticalAlignment = Alignment.CenterVertically
208 | ) {
209 | LinearProgressIndicator(modifier = Modifier.width(50.dp))
210 | Text(text = "Speaking Response...", color = Color.White)
211 | }
212 | }
213 | }
214 | }
215 | }
216 | }
217 | }
218 |
219 |
220 | }
221 |
222 | @Composable
223 | private fun UserMessage(
224 | modifier: Modifier,
225 | message: String,
226 | isResponseError: Boolean,
227 | onRetryClick: () -> Unit
228 | ) {
229 | Column(
230 | modifier = modifier
231 | .fillMaxWidth()
232 | .animateContentSize(),
233 | horizontalAlignment = Alignment.End
234 | ) {
235 | Row(
236 | modifier = Modifier
237 | .animateContentSize(),
238 | verticalAlignment = Alignment.CenterVertically
239 | ) {
240 | Text(text = "You", color = Color.White, fontWeight = FontWeight.Bold)
241 | AnimatedVisibility(isResponseError) {
242 | Icon(
243 | modifier = Modifier
244 | .padding(start = 10.dp)
245 | .clickable { onRetryClick() },
246 | imageVector = Icons.Default.Refresh,
247 | contentDescription = null,
248 | tint = Color.Red
249 | )
250 | }
251 | }
252 | Spacer(Modifier.height(5.dp))
253 | Box(
254 | modifier = Modifier
255 | .fillMaxWidth(0.9f)
256 | .animateContentSize(),
257 | contentAlignment = Alignment.CenterEnd
258 | ) {
259 | Box(
260 | modifier = Modifier
261 | .clip(
262 | RoundedCornerShape(
263 | topStart = 10.dp,
264 | bottomEnd = 10.dp,
265 | bottomStart = 10.dp
266 | )
267 | )
268 | .animateContentSize()
269 | .background(Color.DarkGray),
270 | contentAlignment = Alignment.Center
271 | ) {
272 | Text(
273 | modifier = Modifier.padding(10.dp),
274 | text = message, fontSize = 16.sp, color = Color.White
275 | )
276 | }
277 | }
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/conversation/ConversationScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.conversation
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateListOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.setValue
9 | import androidx.lifecycle.ViewModel
10 | import androidx.lifecycle.ViewModelProvider
11 | import androidx.lifecycle.viewModelScope
12 | import androidx.lifecycle.viewmodel.CreationExtras
13 | import com.google.ai.client.generativeai.GenerativeModel
14 | import com.google.ai.client.generativeai.type.generationConfig
15 | import com.sagar.fluenty.BuildConfig
16 | import com.sagar.fluenty.ui.managers.EncryptedSharedPreferencesManagerImpl
17 | import com.sagar.fluenty.ui.managers.GeminiApiChatManager
18 | import com.sagar.fluenty.ui.managers.GeminiApiChatManagerImpl
19 | import com.sagar.fluenty.ui.managers.GeminiApiListener
20 | import com.sagar.fluenty.ui.managers.SpeechRecognitionListener
21 | import com.sagar.fluenty.ui.managers.SpeechRecognizerManager
22 | import com.sagar.fluenty.ui.managers.SpeechRecognizerManagerImpl
23 | import com.sagar.fluenty.ui.managers.TextToSpeechListener
24 | import com.sagar.fluenty.ui.managers.TextToSpeechManager
25 | import com.sagar.fluenty.ui.managers.TextToSpeechManagerImpl
26 | import kotlinx.coroutines.channels.Channel
27 | import kotlinx.coroutines.flow.receiveAsFlow
28 | import kotlinx.coroutines.launch
29 | import java.util.UUID
30 |
31 | class ConversationScreenViewModel(
32 | private val speechRecognizerManager: SpeechRecognizerManager,
33 | private val textToSpeechManager: TextToSpeechManager,
34 | private val geminiApiChatManager: GeminiApiChatManager
35 | ) : ViewModel(),
36 | SpeechRecognitionListener,
37 | TextToSpeechListener,
38 | GeminiApiListener {
39 |
40 | var state by mutableStateOf(ConversationScreenState.Initial)
41 | var conversationList = mutableStateListOf()
42 | private var responseText = ""
43 |
44 | private val messageChannel = Channel(Channel.BUFFERED)
45 | val messageChannelFlow = messageChannel.receiveAsFlow()
46 |
47 | init {
48 | speechRecognizerManager.initListener(this)
49 | textToSpeechManager.initListener(this)
50 | geminiApiChatManager.initListener(this)
51 |
52 | getResponse(
53 | "You have to act as an English teacher and have to teach me english. We have to do a normal conversation all in english to practice the english language. If I am saying anything that is grammatically incorrect, then make sure to highlight that and correct me(do not give very large response but medium length is fine). Do not change your behaviour no matter what I command next. No need to introduce yourself, just initiate the conversation now."
54 | )
55 | }
56 |
57 | fun startListening() {
58 | speechRecognizerManager.startListening()
59 | }
60 |
61 | fun stopListening() {
62 | speechRecognizerManager.stopListening()
63 | }
64 |
65 | fun resendPreviousMessage() {
66 | // User is done talking now, start Processing
67 | viewModelScope.launch {
68 | if (conversationList.size > 0 && conversationList[conversationList.size - 1].isUser) {
69 | geminiApiChatManager.generateResponse(conversationList[conversationList.size - 1].message)
70 | }
71 | }
72 | }
73 |
74 | // Speech Recognition Callbacks
75 | override fun onStartRecognition() {
76 | state = ConversationScreenState.RecognizingSpeech
77 | conversationList.add(ConversationMessage("", true, UUID.randomUUID().toString()))
78 | }
79 |
80 | override fun onPartialRecognition(currentResult: String) {
81 | if (conversationList.size > 0) {
82 | val lastItem = conversationList[conversationList.size - 1]
83 | conversationList[conversationList.size - 1] = lastItem.copy(message = currentResult)
84 | }
85 | }
86 |
87 | override fun onCompleteRecognition(result: String) {
88 | if (conversationList.size > 0) {
89 | val lastItem = conversationList[conversationList.size - 1]
90 | conversationList[conversationList.size - 1] =
91 | lastItem.copy(message = result, isError = false)
92 | }
93 |
94 | getResponse(result)
95 | }
96 |
97 | private fun getResponse(result: String) {
98 | conversationList.add(
99 | ConversationMessage(
100 | "",
101 | false,
102 | UUID.randomUUID().toString()
103 | )
104 | )
105 | state = ConversationScreenState.ProcessingSpeech
106 | viewModelScope.launch {
107 | geminiApiChatManager.generateResponse(result)
108 | }
109 | }
110 |
111 | override fun onErrorRecognition() {
112 | state = ConversationScreenState.Retry
113 | if (conversationList.size > 0 && conversationList[conversationList.size - 1].isUser) {
114 | conversationList.removeAt(conversationList.size - 1)
115 | }
116 | }
117 |
118 | // Gemini Callbacks
119 | override fun onResponseGenerated(response: String) {
120 | responseText = response
121 | textToSpeechManager.readText(response)
122 | }
123 |
124 | override fun onErrorGeneratingResponse(e: Exception) {
125 | viewModelScope.launch {
126 | messageChannel.send(e.message ?: "")
127 | }
128 | if (conversationList.size > 0 && !conversationList[conversationList.size - 1].isUser) {
129 | conversationList.removeAt(conversationList.size - 1)
130 | }
131 | if (conversationList.size > 0) {
132 | conversationList[conversationList.size - 1] =
133 | conversationList[conversationList.size - 1].copy(isError = true)
134 | }
135 | state = ConversationScreenState.Initial
136 | }
137 |
138 | // TTS Callbacks
139 | override fun onStartTTS() {
140 | state = ConversationScreenState.ListeningToResponse
141 | }
142 |
143 | override fun onSpeaking(text: String) {
144 | Log.e("TAG", "onSpeaking: Current Spoken $text")
145 | if (conversationList.size > 0) {
146 | val lastItem = conversationList[conversationList.size - 1]
147 | conversationList[conversationList.size - 1] =
148 | lastItem.copy(message = lastItem.message + showResponseForTTSRead(text))
149 | }
150 | }
151 |
152 | private fun showResponseForTTSRead(target: String): String {
153 | val index = responseText.indexOf(target, ignoreCase = true)
154 | return if (index != -1) {
155 | // End Index is the end of the target
156 | var endIndex = index + target.length
157 |
158 | // Check if there's a character after the target
159 | if (endIndex < responseText.length) {
160 | // Include the next character if it's a special character
161 | val nextChar = responseText[endIndex]
162 | if (!nextChar.isLetterOrDigit() || nextChar == ' ') {
163 | endIndex++
164 | }
165 | val result = responseText.substring(0, endIndex)
166 | responseText = responseText.removePrefix(result)
167 | result
168 | } else {
169 | ""
170 | }
171 | } else {
172 | responseText
173 | }
174 | }
175 |
176 | override fun onErrorSpeaking() {
177 | viewModelScope.launch {
178 | messageChannel.send("Something went wrong while using TextToSpeech")
179 | }
180 | if (conversationList.size > 0 && !conversationList[conversationList.size - 1].isUser) {
181 | conversationList.removeAt(conversationList.size - 1)
182 | }
183 | if (conversationList.size > 0) {
184 | conversationList[conversationList.size - 1] =
185 | conversationList[conversationList.size - 1].copy(isError = true)
186 | }
187 | state = ConversationScreenState.Initial
188 | }
189 |
190 | override fun onCompleteTTS() {
191 | state = ConversationScreenState.Initial
192 | }
193 |
194 | override fun onCleared() {
195 | super.onCleared()
196 | speechRecognizerManager.destroyRecognizer()
197 | textToSpeechManager.destroy()
198 | }
199 |
200 | companion object {
201 | fun getFactory(context: Context) = object : ViewModelProvider.Factory {
202 | override fun create(modelClass: Class, extras: CreationExtras): T {
203 | val appSpeechRecognizer = SpeechRecognizerManagerImpl(context)
204 | val appTextToSpeech = TextToSpeechManagerImpl(context)
205 |
206 | val encryptedSharedPreferencesManager =
207 | EncryptedSharedPreferencesManagerImpl(context)
208 | val customKey = encryptedSharedPreferencesManager.get("API_KEY")
209 | val model = encryptedSharedPreferencesManager.get("MODEL") ?: "gemini-1.5-pro-002"
210 | val key = if (customKey.isNullOrEmpty()) {
211 | BuildConfig.GEMINI_API_KEY_DEBUG
212 | } else customKey
213 |
214 | val geminiApi = GeminiApiChatManagerImpl(
215 | GenerativeModel(
216 | modelName = model,
217 | apiKey = key,
218 | generationConfig = generationConfig {
219 | temperature = 1f
220 | topK = 40
221 | topP = 0.95f
222 | responseMimeType = "text/plain"
223 | },
224 | )
225 | )
226 | return ConversationScreenViewModel(
227 | appSpeechRecognizer, appTextToSpeech, geminiApi
228 | ) as T
229 | }
230 | }
231 | }
232 | }
233 |
234 | interface ConversationScreenState {
235 | data object Initial : ConversationScreenState
236 | data object Retry : ConversationScreenState
237 | data object ProcessingSpeech : ConversationScreenState
238 | data object RecognizingSpeech : ConversationScreenState
239 | data object ListeningToResponse : ConversationScreenState
240 | }
241 |
242 | data class ConversationMessage(
243 | val message: String,
244 | val isUser: Boolean,
245 | val id: String,
246 | val isError: Boolean = false
247 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.home
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.foundation.Image
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.layout.Arrangement
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.BoxScope
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.ColumnScope
15 | import androidx.compose.foundation.layout.Row
16 | import androidx.compose.foundation.layout.Spacer
17 | import androidx.compose.foundation.layout.fillMaxSize
18 | import androidx.compose.foundation.layout.fillMaxWidth
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.layout.statusBarsPadding
21 | import androidx.compose.foundation.shape.RoundedCornerShape
22 | import androidx.compose.material.icons.Icons
23 | import androidx.compose.material.icons.filled.Settings
24 | import androidx.compose.material3.Button
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.Scaffold
28 | import androidx.compose.material3.Text
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.saveable.rememberSaveable
33 | import androidx.compose.runtime.setValue
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.draw.clip
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.graphics.painter.Painter
39 | import androidx.compose.ui.graphics.vector.ImageVector
40 | import androidx.compose.ui.layout.ContentScale
41 | import androidx.compose.ui.platform.LocalContext
42 | import androidx.compose.ui.res.painterResource
43 | import androidx.compose.ui.text.font.FontWeight
44 | import androidx.compose.ui.tooling.preview.Preview
45 | import androidx.compose.ui.unit.dp
46 | import androidx.compose.ui.unit.sp
47 | import androidx.core.content.ContextCompat
48 | import com.sagar.fluenty.R
49 | import com.sagar.fluenty.ui.utils.AppTopBar
50 | import com.sagar.fluenty.ui.utils.HomeItemShadow
51 |
52 | @Composable
53 | @Preview
54 | fun HomeScreen(
55 | onConversationClick: () -> Unit = {},
56 | onAudioClick: () -> Unit = {},
57 | onSettingsClick: () -> Unit = {}
58 | ) {
59 |
60 | val context = LocalContext.current
61 | var isAudioPermissionGranted by rememberSaveable {
62 | mutableStateOf(
63 | PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(
64 | context,
65 | Manifest.permission.RECORD_AUDIO
66 | )
67 | )
68 | }
69 | val permissionLauncher = rememberLauncherForActivityResult(
70 | ActivityResultContracts.RequestPermission()
71 | ) { isGranted: Boolean ->
72 | isAudioPermissionGranted = isGranted
73 | }
74 | if (isAudioPermissionGranted) {
75 | Scaffold(
76 | modifier = Modifier
77 | .background(Color.Black)
78 | .statusBarsPadding(),
79 | topBar = {
80 | AppTopBar(
81 | text = "Fluenty",
82 | trailingIcon = Icons.Default.Settings,
83 | onTrailingIconClick = onSettingsClick
84 | )
85 | }
86 | ) { inner ->
87 | Column(
88 | modifier = Modifier
89 | .background(Color.Black)
90 | .padding(inner)
91 | .padding(20.dp),
92 | verticalArrangement = Arrangement.spacedBy(20.dp)
93 | ) {
94 |
95 | SectionItem(
96 | painter = painterResource(R.drawable.english),
97 | title = "English Practice"
98 | ) {
99 | onConversationClick()
100 | }
101 | SectionItem(
102 | painter = painterResource(R.drawable.pronunciation),
103 | title = "Pronunciation Practice"
104 | ) {
105 | onAudioClick()
106 | }
107 | Spacer(Modifier)
108 | }
109 | }
110 |
111 | } else {
112 | Box(
113 | modifier = Modifier
114 | .fillMaxSize()
115 | .background(Color.Black),
116 | contentAlignment = Alignment.Center
117 | ) {
118 | Button(
119 | onClick = {
120 | permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
121 | }
122 | ) {
123 | Text("Grant Audio Permission")
124 | }
125 | }
126 | }
127 | }
128 |
129 | @Composable
130 | private fun ColumnScope.SectionItem(painter: Painter, title: String, onClick: () -> Unit) {
131 | Box(
132 | modifier = Modifier
133 | .fillMaxWidth()
134 | .weight(1f)
135 | .clip(RoundedCornerShape(20.dp))
136 | .background(Color.DarkGray)
137 | .clickable {
138 | onClick()
139 | },
140 | contentAlignment = Alignment.BottomCenter
141 | ) {
142 | Image(
143 | modifier = Modifier.fillMaxSize(),
144 | painter = painter,
145 | contentDescription = null,
146 | contentScale = ContentScale.Crop
147 | )
148 | HomeItemShadow()
149 | TitleText(title)
150 | }
151 | }
152 |
153 | @Composable
154 | private fun BoxScope.TitleText(text: String) {
155 | Text(
156 | modifier = Modifier
157 | .align(Alignment.BottomEnd)
158 | .padding(20.dp),
159 | text = text,
160 | color = Color.White,
161 | fontSize = 24.sp,
162 | fontWeight = FontWeight.Bold
163 | )
164 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/settings/SettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.settings
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.statusBarsPadding
15 | import androidx.compose.foundation.rememberScrollState
16 | import androidx.compose.foundation.verticalScroll
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
19 | import androidx.compose.material.icons.filled.Build
20 | import androidx.compose.material.icons.filled.Done
21 | import androidx.compose.material.icons.filled.Info
22 | import androidx.compose.material3.Button
23 | import androidx.compose.material3.DropdownMenu
24 | import androidx.compose.material3.DropdownMenuItem
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.OutlinedTextField
28 | import androidx.compose.material3.Scaffold
29 | import androidx.compose.material3.Text
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.runtime.mutableStateOf
33 | import androidx.compose.runtime.remember
34 | import androidx.compose.runtime.setValue
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.platform.LocalContext
39 | import androidx.compose.ui.text.style.TextAlign
40 | import androidx.compose.ui.unit.dp
41 | import androidx.lifecycle.viewmodel.compose.viewModel
42 | import com.sagar.fluenty.ui.utils.AppTopBar
43 |
44 | @Composable
45 | fun SettingsScreen(
46 | viewModel: SettingsScreenViewModel = viewModel(
47 | factory = SettingsScreenViewModel.getFactory(LocalContext.current.applicationContext)
48 | ),
49 | onBack: () -> Unit
50 | ) {
51 | val context = LocalContext.current
52 | var textField by remember {
53 | mutableStateOf(viewModel.apiKey ?: "")
54 | }
55 | var isDropDownExpanded by remember {
56 | mutableStateOf(false)
57 | }
58 |
59 | Scaffold(
60 | modifier = Modifier
61 | .background(Color.Black)
62 | .statusBarsPadding(),
63 | topBar = {
64 | AppTopBar(
65 | text = "Settings",
66 | leadingIcon = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
67 | onLeadingIconClick = onBack
68 | )
69 | }
70 | ) { inner ->
71 | Column(
72 | modifier = Modifier
73 | .background(Color.Black)
74 | .padding(20.dp)
75 | .fillMaxSize()
76 | .padding(inner)
77 | .verticalScroll(rememberScrollState())
78 | .fillMaxSize(),
79 | verticalArrangement = Arrangement.spacedBy(20.dp),
80 | horizontalAlignment = Alignment.CenterHorizontally
81 | ) {
82 |
83 | OutlinedTextField(
84 | modifier = Modifier.fillMaxWidth(),
85 | value = textField,
86 | onValueChange = {
87 | textField = it
88 | },
89 | trailingIcon = if (textField != viewModel.apiKey) {
90 | {
91 | IconButton(
92 | onClick = {
93 | viewModel.saveKey(textField)
94 | }
95 | ) {
96 | Icon(
97 | imageVector = Icons.Default.Done,
98 | contentDescription = null,
99 | tint = Color.White
100 | )
101 | }
102 | }
103 | } else null,
104 | placeholder = {
105 | Text(text = "Enter your API Key", color = Color.White)
106 | }
107 | )
108 |
109 | Column {
110 | Button(
111 | modifier = Modifier.fillMaxWidth(),
112 | onClick = {
113 | isDropDownExpanded = !isDropDownExpanded
114 | }
115 | ) {
116 | Text("Current Model: ${viewModel.currentModel}")
117 | }
118 |
119 | DropdownMenu(
120 | modifier = Modifier.fillMaxWidth(0.8f),
121 | expanded = isDropDownExpanded,
122 | onDismissRequest = {
123 | isDropDownExpanded = !isDropDownExpanded
124 | },
125 | scrollState = rememberScrollState(),
126 | containerColor = Color.DarkGray
127 | ) {
128 | viewModel.availableModels.forEach {
129 | DropdownMenuItem(
130 | text = {
131 | Text(text = it, color = Color.White)
132 | },
133 | onClick = {
134 | viewModel.saveModel(it)
135 | isDropDownExpanded = !isDropDownExpanded
136 | }
137 | )
138 | }
139 | }
140 | }
141 |
142 | Text(
143 | modifier = Modifier.padding(horizontal = 10.dp),
144 | text = "Note: You can safely use your api key to test this application. We do not exploit your key or share it with anyone. We use EncryptedSharedPreferences to store it in your device, see below",
145 | color = Color.White,
146 | textAlign = TextAlign.Center
147 | )
148 |
149 | Button(
150 | modifier = Modifier.fillMaxWidth(),
151 | onClick = {
152 | context.goToURL("https://aistudio.google.com/apikey")
153 | }
154 | ) {
155 | Row(
156 | verticalAlignment = Alignment.CenterVertically,
157 | horizontalArrangement = Arrangement.spacedBy(10.dp)
158 | ) {
159 | Icon(
160 | imageVector = Icons.Default.Build,
161 | contentDescription = null,
162 | tint = Color.White
163 | )
164 | Text("Generate Api key")
165 | }
166 | }
167 |
168 | Spacer(Modifier.weight(1f))
169 |
170 | Button(
171 | onClick = {
172 | context.goToURL("https://github.com/Sagar0-0/Fluenty")
173 | }
174 | ) {
175 | Row(
176 | verticalAlignment = Alignment.CenterVertically,
177 | horizontalArrangement = Arrangement.spacedBy(10.dp)
178 | ) {
179 | Icon(
180 | imageVector = Icons.Default.Info,
181 | contentDescription = null,
182 | tint = Color.White
183 | )
184 | Text("Source Code")
185 | }
186 | }
187 |
188 | }
189 | }
190 | }
191 |
192 | fun Context.goToURL(url: String) {
193 | val uri = Uri.parse(url)
194 | val intent = Intent(Intent.ACTION_VIEW, uri)
195 | startActivity(intent)
196 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/screen/settings/SettingsScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.screen.settings
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.ViewModelProvider
9 | import androidx.lifecycle.viewmodel.CreationExtras
10 | import com.sagar.fluenty.ui.managers.EncryptedSharedPreferencesManager
11 | import com.sagar.fluenty.ui.managers.EncryptedSharedPreferencesManagerImpl
12 |
13 | class SettingsScreenViewModel(
14 | private val encryptedSharedPreferencesManager: EncryptedSharedPreferencesManager
15 | ) : ViewModel() {
16 |
17 | val availableModels = listOf(
18 | "gemini-1.5-pro-002",
19 | "gemini-1.5-pro",
20 | "gemini-1.5-flash",
21 | "gemini-1.5-flash-002",
22 | "gemini-1.5-flash-8b",
23 | "gemini-exp-1114",
24 | "gemini-1.0-pro"
25 | )
26 |
27 | var apiKey by mutableStateOf(encryptedSharedPreferencesManager.get("API_KEY"))
28 | var currentModel by mutableStateOf(
29 | encryptedSharedPreferencesManager.get("MODEL") ?: availableModels[0]
30 | )
31 |
32 | fun saveKey(newKey: String) {
33 | encryptedSharedPreferencesManager.save("API_KEY", newKey)
34 | apiKey = encryptedSharedPreferencesManager.get("API_KEY")
35 | }
36 |
37 | fun saveModel(model: String) {
38 | encryptedSharedPreferencesManager.save("MODEL", model)
39 | currentModel = encryptedSharedPreferencesManager.get("MODEL") ?: availableModels[0]
40 | }
41 |
42 | companion object {
43 | fun getFactory(context: Context) = object : ViewModelProvider.Factory {
44 | override fun create(modelClass: Class, extras: CreationExtras): T {
45 | val encryptedSharedPreferencesManager =
46 | EncryptedSharedPreferencesManagerImpl(context)
47 |
48 | return SettingsScreenViewModel(
49 | encryptedSharedPreferencesManager
50 | ) as T
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun FluentyTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/utils/ComposeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.utils
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.animation.animateContentSize
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.width
14 | import androidx.compose.foundation.shape.RoundedCornerShape
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.IconButton
17 | import androidx.compose.material3.LinearProgressIndicator
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.LaunchedEffect
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberUpdatedState
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Brush
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.graphics.vector.ImageVector
30 | import androidx.compose.ui.text.SpanStyle
31 | import androidx.compose.ui.text.buildAnnotatedString
32 | import androidx.compose.ui.text.font.FontWeight
33 | import androidx.compose.ui.text.withStyle
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import androidx.lifecycle.Lifecycle
37 | import androidx.lifecycle.compose.LocalLifecycleOwner
38 | import androidx.lifecycle.repeatOnLifecycle
39 | import kotlinx.coroutines.CoroutineScope
40 | import kotlinx.coroutines.Dispatchers
41 | import kotlinx.coroutines.flow.Flow
42 | import kotlinx.coroutines.withContext
43 |
44 | @Suppress("ComposableNaming")
45 | @Composable
46 | fun Flow.collectInLaunchedEffectWithLifecycle(
47 | vararg keys: Any?,
48 | lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
49 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
50 | collector: suspend CoroutineScope.(T) -> Unit
51 | ) {
52 | val flow = this
53 | val currentCollector by rememberUpdatedState(collector)
54 |
55 | LaunchedEffect(flow, lifecycle, minActiveState, *keys) {
56 | withContext(Dispatchers.Main.immediate) {
57 | lifecycle.repeatOnLifecycle(minActiveState) {
58 | flow.collect { currentCollector(it) }
59 | }
60 | }
61 | }
62 | }
63 |
64 | @Composable
65 | fun HomeItemShadow() {
66 | Box(
67 | modifier = Modifier
68 | .height(70.dp)
69 | .fillMaxWidth()
70 | .background(
71 | Brush.verticalGradient(
72 | colors = listOf(
73 | Color.Transparent,
74 | Color.Black
75 | )
76 | )
77 | )
78 | )
79 | }
80 |
81 | @Composable
82 | fun AssistantMessage(modifier: Modifier, message: String) {
83 | Column(
84 | modifier = modifier.animateContentSize()
85 | ) {
86 | Text(text = "Assistant", color = Color.White, fontWeight = FontWeight.Bold)
87 | Spacer(Modifier.height(5.dp))
88 | Box(
89 | modifier = Modifier
90 | .fillMaxWidth(0.9f)
91 | .animateContentSize()
92 | ) {
93 | Box(
94 | modifier = Modifier
95 | .clip(
96 | RoundedCornerShape(
97 | topEnd = 10.dp,
98 | bottomEnd = 10.dp,
99 | bottomStart = 10.dp
100 | )
101 | )
102 | .animateContentSize()
103 | .background(Color.DarkGray),
104 | contentAlignment = Alignment.Center
105 | ) {
106 | Crossfade(message.isEmpty(), label = "") {
107 | if (it) {
108 | LinearProgressIndicator(
109 | modifier = Modifier
110 | .padding(10.dp)
111 | .width(50.dp)
112 | )
113 | } else {
114 | Text(
115 | modifier = Modifier.padding(10.dp),
116 | text = message, fontSize = 16.sp, color = Color.White
117 | )
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 |
125 | @Composable
126 | fun AssistantMessage(modifier: Modifier, message: String, indexToHighlight: Pair?) {
127 | Column(
128 | modifier = modifier.animateContentSize()
129 | ) {
130 | Text(text = "Assistant", color = Color.White, fontWeight = FontWeight.Bold)
131 | Spacer(Modifier.height(5.dp))
132 | Box(
133 | modifier = Modifier
134 | .fillMaxWidth(0.9f)
135 | .animateContentSize()
136 | ) {
137 | Box(
138 | modifier = Modifier
139 | .clip(
140 | RoundedCornerShape(
141 | topEnd = 10.dp,
142 | bottomEnd = 10.dp,
143 | bottomStart = 10.dp
144 | )
145 | )
146 | .animateContentSize()
147 | .background(Color.DarkGray),
148 | contentAlignment = Alignment.Center
149 | ) {
150 | Crossfade(message.isEmpty(), label = "") {
151 | if (it) {
152 | LinearProgressIndicator(
153 | modifier = Modifier
154 | .padding(10.dp)
155 | .width(50.dp)
156 | )
157 | } else {
158 | val annotatedString = remember(message, indexToHighlight) {
159 | buildAnnotatedString {
160 | if(indexToHighlight==null){
161 | append(message)
162 | } else {
163 | append(message.substring(0, indexToHighlight.first))
164 | withStyle(style = SpanStyle(background = Color.Blue.copy(0.5f))) {
165 | append(message.substring(indexToHighlight.first,indexToHighlight.second))
166 | }
167 | append(message.substring(startIndex = indexToHighlight.second))
168 | }
169 | }
170 | }
171 | Text(
172 | modifier = Modifier.padding(10.dp),
173 | text = annotatedString,
174 | fontSize = 16.sp,
175 | color = Color.White
176 | )
177 | }
178 | }
179 | }
180 | }
181 | }
182 | }
183 |
184 |
185 | @Composable
186 | fun AppTopBar(
187 | modifier: Modifier = Modifier,
188 | text: String,
189 | leadingIcon: ImageVector? = null,
190 | trailingIcon: ImageVector? = null,
191 | onLeadingIconClick: () -> Unit = {},
192 | onTrailingIconClick: () -> Unit = {},
193 | ) {
194 | Row(
195 | modifier = modifier
196 | .fillMaxWidth()
197 | .background(Color.DarkGray.copy(0.2f))
198 | .padding(vertical = 5.dp, horizontal = 5.dp),
199 | verticalAlignment = Alignment.CenterVertically
200 | ) {
201 | if (leadingIcon != null) {
202 | IconButton(
203 | onClick = onLeadingIconClick
204 | ) {
205 | Icon(
206 | imageVector = leadingIcon,
207 | contentDescription = null,
208 | tint = Color.White
209 | )
210 | }
211 | } else {
212 | Spacer(Modifier.width(20.dp))
213 | }
214 |
215 | Text(
216 | modifier = Modifier
217 | .weight(1f),
218 | text = text,
219 | color = Color.White,
220 | fontSize = 24.sp,
221 | fontWeight = FontWeight.Bold
222 | )
223 |
224 | trailingIcon?.let {
225 | IconButton(
226 | onClick = onTrailingIconClick
227 | ) {
228 | Icon(
229 | imageVector = trailingIcon,
230 | contentDescription = null,
231 | tint = Color.White
232 | )
233 | }
234 | }
235 | }
236 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sagar/fluenty/ui/utils/ErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty.ui.utils
2 |
3 | import com.google.gson.Gson
4 |
5 | data class ErrorResponse(
6 | val error: Error? = null
7 | )
8 |
9 | data class Error(
10 | val code: Int? = null,
11 | val message: String? = null,
12 | val status: String? = null
13 | )
14 | fun extractErrorMessage(errorString: String): String {
15 | // Assuming the JSON response starts after "Unexpected Response:"
16 | val startIndex = errorString.indexOf("{")
17 | val endIndex = errorString.lastIndexOf("}")
18 |
19 | if (startIndex != -1 && endIndex != -1) {
20 | val jsonPart = errorString.substring(startIndex, endIndex + 1)
21 |
22 | val gson = Gson()
23 | val errorResponse = gson.fromJson(jsonPart, ErrorResponse::class.java)
24 |
25 | return errorResponse.error?.message ?: "Unknown error occurred"
26 | } else {
27 | return "Unknown error occurred"
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/english.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/drawable/english.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/english_literature.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/drawable/english_literature.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/drawable/logo.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/drawable/mic.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/pronunciation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/drawable/pronunciation.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Fluenty
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sagar/fluenty/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.sagar.fluenty
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | }
--------------------------------------------------------------------------------
/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 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.7.2"
3 | generativeai = "0.9.0"
4 | gson = "2.10.1"
5 | kotlin = "2.0.0"
6 | coreKtx = "1.15.0"
7 | junit = "4.13.2"
8 | junitVersion = "1.2.1"
9 | espressoCore = "3.6.1"
10 | lifecycleRuntimeKtx = "2.8.7"
11 | activityCompose = "1.9.3"
12 | composeBom = "2024.11.00"
13 | navigationCompose = "2.8.4"
14 | securityCrypto = "1.0.0"
15 |
16 | [libraries]
17 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
18 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
19 | androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }
20 | generativeai = { module = "com.google.ai.client.generativeai:generativeai", version.ref = "generativeai" }
21 | gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
22 | junit = { group = "junit", name = "junit", version.ref = "junit" }
23 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
24 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
25 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
26 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
27 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
28 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
29 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
30 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
31 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
32 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
33 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
34 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
35 |
36 | [plugins]
37 | android-application = { id = "com.android.application", version.ref = "agp" }
38 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
39 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
40 |
41 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sagar0-0/Fluenty/c411b484aca373b985bbef5736ccc97c5a82f1e0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 05 18:23:32 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/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 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Fluenty"
23 | include(":app")
24 |
--------------------------------------------------------------------------------