From 2ee06e8375ef0767053124e0484a5e78e9a87a41 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Fri, 1 Apr 2022 11:45:49 +0200 Subject: [PATCH 01/42] Start a SpeechRecognizer screen. -Integrate SpeechRecognizer(callback) in JetPackCompose (using flows) --- gradle/libs.versions.toml | 3 + mobile/src/main/AndroidManifest.xml | 4 + .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 6 + .../java/ch/epfl/sdp/mobile/ui/Graphics.kt | 6 + .../epfl/sdp/mobile/ui/home/HomeScaffold.kt | 3 +- .../ch/epfl/sdp/mobile/ui/i18n/English.kt | 1 + .../sdp/mobile/ui/i18n/LocalizedStrings.kt | 1 + .../SpeechRecognitionScreen.kt | 120 ++++++++++++++++++ .../ui/speech_recognition/SpeechRecognizer.kt | 8 ++ 9 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6636c757..95af9ef64 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ compose = "1.1.1" coroutines = "1.6.0" jacoco = "0.8.8-SNAPSHOT" # Custom JaCoCo release with Jetpack Compose support. mockk = "1.12.3" +accompanist = "0.23.1" [libraries] @@ -30,6 +31,7 @@ compose-material-material = { module = "androidx.compose.material:material", ver compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose" } compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } +compose-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -67,6 +69,7 @@ compose-android = [ "compose-material-ripple", "compose-material-icons-core", "compose-material-icons-extended", + "compose-accompanist-permissions", ] coroutines-android = [ diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 426c8fc84..2de3697f3 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + HomeSection.Settings ArRoute -> HomeSection.Ar PlayRoute -> HomeSection.Play + SpeechRecognitionRoute -> HomeSection.SpeechRecognition else -> HomeSection.Social } @@ -110,6 +115,7 @@ private fun HomeSection.toRoute(): String = HomeSection.Settings -> SettingsRoute HomeSection.Ar -> ArRoute HomeSection.Play -> PlayRoute + HomeSection.SpeechRecognition -> SpeechRecognitionRoute } private fun hideBar(route: String?): Boolean { diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt index f40051a05..623273678 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt @@ -45,6 +45,12 @@ val PawniesIcons.Search val PawniesIcons.Check get() = Icons.Default.Check +val PawniesIcons.Mic + get() = Icons.Default.Mic + +val PawniesIcons.MicOff + get() = Icons.Default.MicOff + /** Chess pieces */ object ChessIcons diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt index 4ca85903c..a07248e31 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt @@ -26,7 +26,8 @@ enum class HomeSection( /** The section to manage our preferences. */ Settings(PawniesIcons.SectionSettings, { sectionSettings }), - Ar(PawniesIcons.ArView, { sectionAr }) + Ar(PawniesIcons.ArView, { sectionAr }), + SpeechRecognition(PawniesIcons.Mic, { sectionSpeechRecognition }), } /** diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt index 4b9e5111b..cded4d028 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt @@ -56,6 +56,7 @@ object English : LocalizedStrings { override val sectionSocial = "Players" override val sectionSettings = "Settings" override val sectionPlay = "Play" + override val sectionSpeechRecognition = "Vocal" override val newGame = "New game".uppercase() diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt index cb5ceb5ae..169554343 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt @@ -60,6 +60,7 @@ interface LocalizedStrings { val sectionSocial: String val sectionSettings: String val sectionPlay: String + val sectionSpeechRecognition: String val newGame: String val prepareGameChooseColor: String diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt new file mode 100644 index 000000000..019ebd16b --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -0,0 +1,120 @@ +package ch.epfl.sdp.mobile.ui.speech_recognition + +import android.Manifest +import android.content.Intent +import android.os.Bundle +import android.speech.RecognitionListener +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import android.speech.SpeechRecognizer.createSpeechRecognizer +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import ch.epfl.sdp.mobile.ui.* +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import java.util.* + +@Preview(backgroundColor = 0xFFFFFBE6, showBackground = true) +@Composable +fun Preview_mic() { + PawniesTheme { SpeechRecognitionScreen() } +} + +@Composable +@OptIn(ExperimentalPermissionsApi::class) +fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { + + val text = remember { mutableStateOf("---") } + val microPermissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) + var startSpeech by remember { mutableStateOf(false) } + var speechRecognizer: SpeechRecognizer? = null + + if (startSpeech) { + speechRecognizer = startRecognition(text) + } + + val microIcon = if (microPermissionState.hasPermission) PawniesIcons.Mic else PawniesIcons.MicOff + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier) { + // Speech Text + Text(text.value, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(32.dp)) + OutlinedButton( + shape = CircleShape, + onClick = { + microPermissionState.launchPermissionRequest() + if (microPermissionState.hasPermission) { + startSpeech = true + } else { + microPermissionState.launchPermissionRequest() + } + if(startSpeech){ + startSpeech = false + speechRecognizer?.stopListening() + } + }) { Icon(microIcon, null) } + Spacer(modifier = Modifier.height(32.dp)) + PermissionText( + hasPermission = microPermissionState.hasPermission, + ) + } +} + +@Composable +private fun PermissionText(hasPermission: Boolean = false, modifier: Modifier = Modifier) { + val text = if (hasPermission) "Permission has been granted ! " else "Permission was NOT GRANTED !" + Text(text = text, textAlign = TextAlign.Center, modifier = modifier) +} + +@Composable +private fun startRecognition(text: MutableState): SpeechRecognizer { + + val speechRecognizer = createSpeechRecognizer(LocalContext.current) + val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + speechRecognizerIntent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault()) + + speechRecognizer.setRecognitionListener( + object : RecognitionListener { + override fun onReadyForSpeech(params: Bundle?) {} + override fun onBeginningOfSpeech() { + text.value = "Listening..." + } + override fun onResults(results: Bundle?) { + val speech = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) + text.value = speech?.get(0)!! + } + + override fun onRmsChanged(rmsdB: Float) {} + + override fun onBufferReceived(buffer: ByteArray?) {} + + override fun onEndOfSpeech() {} + + override fun onError(error: Int) {} + + override fun onPartialResults(partialResults: Bundle?) {} + + override fun onEvent(eventType: Int, params: Bundle?) {} + }) + + speechRecognizer.startListening(speechRecognizerIntent) + return speechRecognizer +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt new file mode 100644 index 000000000..6d697e4e5 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt @@ -0,0 +1,8 @@ +package ch.epfl.sdp.mobile.ui.speech_recognition + +class SpeechRecognizer { + + + +} + From c79fb6bb60abe584db5f76486b2a61e04d5d71b4 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Fri, 1 Apr 2022 12:14:45 +0200 Subject: [PATCH 02/42] Format --- .../ui/speech_recognition/SpeechRecognitionScreen.kt | 2 +- .../sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 019ebd16b..811ebe271 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -64,7 +64,7 @@ fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { } else { microPermissionState.launchPermissionRequest() } - if(startSpeech){ + if (startSpeech) { startSpeech = false speechRecognizer?.stopListening() } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt index 6d697e4e5..29414a329 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt @@ -1,8 +1,3 @@ package ch.epfl.sdp.mobile.ui.speech_recognition -class SpeechRecognizer { - - - -} - +class SpeechRecognizer {} From 3b303b0407fcd0085c120c8ed4a5b8b489a8efd5 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Fri, 1 Apr 2022 19:58:14 +0200 Subject: [PATCH 03/42] First working version of speech recognizer --- .../SpeechRecognitionScreen.kt | 120 +++++++++--------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 811ebe271..2bb7bd085 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -1,12 +1,12 @@ package ch.epfl.sdp.mobile.ui.speech_recognition import android.Manifest +import android.content.Context import android.content.Intent import android.os.Bundle import android.speech.RecognitionListener import android.speech.RecognizerIntent import android.speech.SpeechRecognizer -import android.speech.SpeechRecognizer.createSpeechRecognizer import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -20,101 +20,107 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ch.epfl.sdp.mobile.ui.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import java.util.* +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine -@Preview(backgroundColor = 0xFFFFFBE6, showBackground = true) -@Composable -fun Preview_mic() { - PawniesTheme { SpeechRecognitionScreen() } -} +const val Lang = "en-US" @Composable @OptIn(ExperimentalPermissionsApi::class) fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { - val text = remember { mutableStateOf("---") } + val context = LocalContext.current + var text by remember { mutableStateOf("---") } val microPermissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) - var startSpeech by remember { mutableStateOf(false) } - var speechRecognizer: SpeechRecognizer? = null - - if (startSpeech) { - speechRecognizer = startRecognition(text) - } + val activeSpeech = remember { mutableStateOf(false) } - val microIcon = if (microPermissionState.hasPermission) PawniesIcons.Mic else PawniesIcons.MicOff + val microIcon = if (activeSpeech.value) PawniesIcons.Mic else PawniesIcons.MicOff Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { + // Speech Text - Text(text.value, textAlign = TextAlign.Center) + Text(text, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(32.dp)) + + // Microphone Button OutlinedButton( shape = CircleShape, onClick = { - microPermissionState.launchPermissionRequest() - if (microPermissionState.hasPermission) { - startSpeech = true - } else { + if (!microPermissionState.hasPermission) { microPermissionState.launchPermissionRequest() } - if (startSpeech) { - startSpeech = false - speechRecognizer?.stopListening() - } + activeSpeech.value = !activeSpeech.value && microPermissionState.hasPermission + text = if (activeSpeech.value) "Listening" else "---" }) { Icon(microIcon, null) } + Spacer(modifier = Modifier.height(32.dp)) + + // Display information about vocal permission PermissionText( hasPermission = microPermissionState.hasPermission, ) } + if (activeSpeech.value) { + LaunchedEffect(key1 = activeSpeech) { + val txt = recognition(context) + text = txt + activeSpeech.value = false + } + } } @Composable -private fun PermissionText(hasPermission: Boolean = false, modifier: Modifier = Modifier) { +private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean = false) { val text = if (hasPermission) "Permission has been granted ! " else "Permission was NOT GRANTED !" Text(text = text, textAlign = TextAlign.Center, modifier = modifier) } -@Composable -private fun startRecognition(text: MutableState): SpeechRecognizer { - - val speechRecognizer = createSpeechRecognizer(LocalContext.current) - val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) - speechRecognizerIntent.putExtra( - RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) - speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault()) - - speechRecognizer.setRecognitionListener( - object : RecognitionListener { - override fun onReadyForSpeech(params: Bundle?) {} - override fun onBeginningOfSpeech() { - text.value = "Listening..." - } +// Returns speech result from the recognizer +suspend fun recognition(context: Context): String = suspendCancellableCoroutine { cont -> + val recognizer = SpeechRecognizer.createSpeechRecognizer(context) + val speechRecognizerIntent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action + .putExtra(RecognizerIntent.EXTRA_LANGUAGE, Lang) // Speech language + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1) // Number of results + + // Listener for results + val listener = + object : RecognitionListenerAdapter() { override fun onResults(results: Bundle?) { - val speech = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) - text.value = speech?.get(0)!! + super.onResults(results) + if (results == null) { + cont.resume("") + return + } + cont.resume(results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)!![0]) } + } + recognizer.setRecognitionListener(listener) + recognizer.startListening(speechRecognizerIntent) + + // Clearing upon coroutine cancellation + cont.invokeOnCancellation { + recognizer.stopListening() + recognizer.destroy() + } +} - override fun onRmsChanged(rmsdB: Float) {} - - override fun onBufferReceived(buffer: ByteArray?) {} - - override fun onEndOfSpeech() {} - - override fun onError(error: Int) {} - - override fun onPartialResults(partialResults: Bundle?) {} - - override fun onEvent(eventType: Int, params: Bundle?) {} - }) - - speechRecognizer.startListening(speechRecognizerIntent) - return speechRecognizer +abstract class RecognitionListenerAdapter : RecognitionListener { + override fun onReadyForSpeech(params: Bundle?) = Unit + override fun onBeginningOfSpeech() = Unit + override fun onRmsChanged(rmsdB: Float) = Unit + override fun onBufferReceived(buffer: ByteArray?) = Unit + override fun onEndOfSpeech() = Unit + override fun onError(error: Int) = Unit + override fun onResults(results: Bundle?) = Unit + override fun onPartialResults(partialResults: Bundle?) = Unit + override fun onEvent(eventType: Int, params: Bundle?) = Unit } From 5701942befe8072205536074211d8c6c0a410bca Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Fri, 1 Apr 2022 20:25:43 +0200 Subject: [PATCH 04/42] Increase Number of results to 10 --- .../ui/speech_recognition/SpeechRecognitionScreen.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 2bb7bd085..e1744ab9f 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -24,11 +24,11 @@ import androidx.compose.ui.unit.dp import ch.epfl.sdp.mobile.ui.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState -import java.util.* import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine const val Lang = "en-US" +const val Max_results = 10 @Composable @OptIn(ExperimentalPermissionsApi::class) @@ -71,7 +71,7 @@ fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { if (activeSpeech.value) { LaunchedEffect(key1 = activeSpeech) { val txt = recognition(context) - text = txt + text = txt.joinToString(separator = "\n") activeSpeech.value = false } } @@ -84,12 +84,12 @@ private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean } // Returns speech result from the recognizer -suspend fun recognition(context: Context): String = suspendCancellableCoroutine { cont -> +suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> val recognizer = SpeechRecognizer.createSpeechRecognizer(context) val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action .putExtra(RecognizerIntent.EXTRA_LANGUAGE, Lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1) // Number of results + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, Max_results) // Number of results // Listener for results val listener = @@ -97,10 +97,10 @@ suspend fun recognition(context: Context): String = suspendCancellableCoroutine override fun onResults(results: Bundle?) { super.onResults(results) if (results == null) { - cont.resume("") + cont.resume(emptyList()) return } - cont.resume(results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)!![0]) + cont.resume(results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)!!) } } recognizer.setRecognitionListener(listener) From 613caae8fe63c8bce5b1cefaaa56917d4202b71c Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Fri, 1 Apr 2022 20:32:00 +0200 Subject: [PATCH 05/42] Fix toml by removing duplicate accompanist value --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01c763de9..e6f3d17d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,6 @@ compose = "1.1.1" coroutines = "1.6.0" jacoco = "0.8.8-SNAPSHOT" # Custom JaCoCo release with Jetpack Compose support. mockk = "1.12.3" -accompanist = "0.23.1" [libraries] From 881d8bce27cdbbab6ff113430745d27846899625 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Sun, 3 Apr 2022 17:52:18 +0200 Subject: [PATCH 06/42] Resolve comments: - Delete unused class - Rename dependency in toml - Add comment about route - Refactor to MaxResultsCount - use rememberCoroutineScope --- gradle/libs.versions.toml | 4 +- .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 1 + .../SpeechRecognitionScreen.kt | 52 ++++++++++--------- .../ui/speech_recognition/SpeechRecognizer.kt | 3 -- 4 files changed, 30 insertions(+), 30 deletions(-) delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6f3d17d9..e87b1789e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.4.0" androidx-test-espresso-intents = "androidx.test.espresso:espresso-intents:3.4.0" accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } compose-animation-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose" } @@ -33,7 +34,6 @@ compose-material-material = { module = "androidx.compose.material:material", ver compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose" } compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } -compose-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -60,6 +60,7 @@ truth = "com.google.truth:truth:1.1.3" compose-android = [ "accompanist-flowlayout", + "accompanist-permissions", "androidx-activity-compose", "androidx-lifecycle-compose", "androidx-navigation-compose", @@ -72,7 +73,6 @@ compose-android = [ "compose-material-ripple", "compose-material-icons-core", "compose-material-icons-extended", - "compose-accompanist-permissions", ] coroutines-android = [ diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 3caf99164..1c178203b 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -36,6 +36,7 @@ private const val GameDefaultId = "" /** The route associated to new game button in play screen */ private const val PrepareGameRoute = "prepare_game" +/** Temporal route associated with speech recognition demo screen */ private const val SpeechRecognitionRoute = "speech" /** diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index e1744ab9f..39395f71b 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -9,8 +9,6 @@ import android.speech.RecognizerIntent import android.speech.SpeechRecognizer import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.OutlinedButton @@ -23,12 +21,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import ch.epfl.sdp.mobile.ui.* import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.rememberPermissionState import kotlin.coroutines.resume +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine const val Lang = "en-US" -const val Max_results = 10 +const val MaxResultsCount = 10 @Composable @OptIn(ExperimentalPermissionsApi::class) @@ -37,43 +37,47 @@ fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { val context = LocalContext.current var text by remember { mutableStateOf("---") } val microPermissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) - val activeSpeech = remember { mutableStateOf(false) } + var activeSpeech by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() - val microIcon = if (activeSpeech.value) PawniesIcons.Mic else PawniesIcons.MicOff + val microIcon = if (activeSpeech) PawniesIcons.Mic else PawniesIcons.MicOff Column( - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { // Speech Text Text(text, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(32.dp)) // Microphone Button OutlinedButton( shape = CircleShape, onClick = { - if (!microPermissionState.hasPermission) { - microPermissionState.launchPermissionRequest() + askForPermission(microPermissionState) + activeSpeech = !activeSpeech && microPermissionState.hasPermission + scope.launch { + if (activeSpeech) { + text = "Listening..." + text = recognition(context).joinToString(separator = "\n") + activeSpeech = false + } else { + text = "---" + } } - activeSpeech.value = !activeSpeech.value && microPermissionState.hasPermission - text = if (activeSpeech.value) "Listening" else "---" }) { Icon(microIcon, null) } - Spacer(modifier = Modifier.height(32.dp)) - // Display information about vocal permission PermissionText( hasPermission = microPermissionState.hasPermission, ) } - if (activeSpeech.value) { - LaunchedEffect(key1 = activeSpeech) { - val txt = recognition(context) - text = txt.joinToString(separator = "\n") - activeSpeech.value = false - } +} + +@OptIn(ExperimentalPermissionsApi::class) +private fun askForPermission(microPermissionState: PermissionState) { + if (!microPermissionState.hasPermission) { + microPermissionState.launchPermissionRequest() } } @@ -89,18 +93,16 @@ suspend fun recognition(context: Context): List = suspendCancellableCoro val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action .putExtra(RecognizerIntent.EXTRA_LANGUAGE, Lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, Max_results) // Number of results + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, MaxResultsCount) // Number of results // Listener for results val listener = object : RecognitionListenerAdapter() { override fun onResults(results: Bundle?) { super.onResults(results) - if (results == null) { - cont.resume(emptyList()) - return - } - cont.resume(results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)!!) + cont.resume( + // results cannot br null + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) } } recognizer.setRecognitionListener(listener) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt deleted file mode 100644 index 29414a329..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizer.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ch.epfl.sdp.mobile.ui.speech_recognition - -class SpeechRecognizer {} From 567dde9e0b3443be3cbaeec8d9565a0232d90da9 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Sun, 3 Apr 2022 18:04:09 +0200 Subject: [PATCH 07/42] Use already defined mic icons --- mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt | 6 ------ .../mobile/ui/speech_recognition/SpeechRecognitionScreen.kt | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt index 8e6a2a612..97927ca73 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/Graphics.kt @@ -58,12 +58,6 @@ val PawniesIcons.Search val PawniesIcons.Check get() = Icons.Default.Check -val PawniesIcons.Mic - get() = Icons.Default.Mic - -val PawniesIcons.MicOff - get() = Icons.Default.MicOff - /** Chess pieces */ object ChessIcons diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 39395f71b..29a45457f 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -40,7 +40,7 @@ fun SpeechRecognitionScreen(modifier: Modifier = Modifier) { var activeSpeech by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - val microIcon = if (activeSpeech) PawniesIcons.Mic else PawniesIcons.MicOff + val microIcon = if (activeSpeech) PawniesIcons.GameMicOn else PawniesIcons.GameMicOff Column( verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically), From ca558c27615930ff9dcd4c4ceae57b3e9e86ba51 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Sun, 3 Apr 2022 18:32:49 +0200 Subject: [PATCH 08/42] Use already defined mic icons --- mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt index a07248e31..0f42d14f9 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt @@ -27,7 +27,7 @@ enum class HomeSection( /** The section to manage our preferences. */ Settings(PawniesIcons.SectionSettings, { sectionSettings }), Ar(PawniesIcons.ArView, { sectionAr }), - SpeechRecognition(PawniesIcons.Mic, { sectionSpeechRecognition }), + SpeechRecognition(PawniesIcons.GameMicOn, { sectionSpeechRecognition }), } /** From 7392e2480240253ec966dbb7548291e30bf351ce Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Mon, 4 Apr 2022 01:59:42 +0200 Subject: [PATCH 09/42] Update mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt Co-authored-by: Alexandre Piveteau --- mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 1c178203b..8f7fb6826 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -36,7 +36,7 @@ private const val GameDefaultId = "" /** The route associated to new game button in play screen */ private const val PrepareGameRoute = "prepare_game" -/** Temporal route associated with speech recognition demo screen */ +/** Temporary route associated with speech recognition demo screen */ private const val SpeechRecognitionRoute = "speech" /** From 4ab1fe3d14d7cefcfaa773e041a694e9c72e47ba Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Mon, 4 Apr 2022 02:02:12 +0200 Subject: [PATCH 10/42] Make constants private --- .../mobile/ui/speech_recognition/SpeechRecognitionScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 29a45457f..162cae8e2 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -27,8 +27,8 @@ import kotlin.coroutines.resume import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -const val Lang = "en-US" -const val MaxResultsCount = 10 +private const val Lang = "en-US" +private const val MaxResultsCount = 10 @Composable @OptIn(ExperimentalPermissionsApi::class) From 184455dc5500b0c62ed89e2ae93714f28ec7c689 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 5 Apr 2022 14:33:08 +0200 Subject: [PATCH 11/42] Add tests and revise permissions in manifest - Add stateful screen - Add tests - Make strings consts --- .../StatefulSpeechRecognitionScreenTest.kt | 57 +++++++++++++++ .../SpeechRecognitionScreenTest.kt | 70 +++++++++++++++++++ mobile/src/main/AndroidManifest.xml | 9 ++- .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 2 +- .../state/StatefulSpeechRecognitionScreen.kt | 24 +++++++ .../SpeechRecognitionScreen.kt | 31 +++++--- .../SpeechRecognitionScreenState.kt | 13 ++++ 7 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt new file mode 100644 index 000000000..63ac52e64 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -0,0 +1,57 @@ +package ch.epfl.sdp.mobile.test.state + +import android.Manifest +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.rule.GrantPermissionRule +import androidx.test.rule.GrantPermissionRule.grant +import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser +import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade +import ch.epfl.sdp.mobile.application.chess.ChessFacade +import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.state.ProvideFacades +import ch.epfl.sdp.mobile.state.StatefulSpeechRecognitionScreen +import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth +import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.ui.speech_recognition.ListeningText +import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionDenied +import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionGranted +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +@ExperimentalPermissionsApi +class StatefulSpeechRecognitionScreenTest { + + @get:Rule + val rule = createComposeRule() + + @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.CAMERA) + + @Test + fun given_defaultScreen_when_micClicked_and_okClicked_then_permissionGranted() = runTest { + val auth = emptyAuth() + val store = emptyStore() + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + + facade.signUpWithEmail("email", "name", "password") + val user = facade.currentUser.filterIsInstance().first() + + rule.setContent { + ProvideFacades(facade, social, chess) { StatefulSpeechRecognitionScreen(user) } + } + + // rule.onNodeWithText(PermissionDenied).assertExists() + rule.onNodeWithText(PermissionGranted).assertExists() + rule.onNodeWithContentDescription("micro").assertExists().performClick() + rule.onNodeWithText(ListeningText).assertExists() + rule.onNodeWithContentDescription("micro").assertExists().performClick() + } +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt new file mode 100644 index 000000000..5f4646f66 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -0,0 +1,70 @@ +package ch.epfl.sdp.mobile.test.ui.speech_recognition + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import ch.epfl.sdp.mobile.ui.speech_recognition.* +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import org.junit.Rule +import org.junit.Test + +private const val GrantText = "ALLOW" +private const val DenyText = "DENY" + +@ExperimentalPermissionsApi +class SpeechRecognitionScreenTest { + @get:Rule val rule = createComposeRule() + + @Test + fun given_defaultScreenState_when_showed_then_displayedDefaultScreen() { + val state = + object : SpeechRecognitionScreenState { + override val microphonePermissionState: MutableState = mutableStateOf(false) + override val onPermissionChange: () -> Unit = { + microphonePermissionState.value = !microphonePermissionState.value + } + } + + rule.setContent { SpeechRecognitionScreen(state) } + rule.onNodeWithText(PermissionDenied).assertDoesNotExist() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists() + rule.onNodeWithText(DefaultText).assertExists() + } + + @Test + fun given_defaultScreenState_when_micClicked_then_displayPermissionDialog() { + val state = + object : SpeechRecognitionScreenState { + override val microphonePermissionState: MutableState = mutableStateOf(false) + override val onPermissionChange: () -> Unit = { + microphonePermissionState.value = !microphonePermissionState.value + } + } + + rule.setContent { SpeechRecognitionScreen(state) } + rule.onNodeWithText(PermissionDenied).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + rule.onNodeWithText(GrantText).assertExists() + } + + @Test + fun given_permissionDialog_when_okClicked_then_grantPermission() { + val state = + object : SpeechRecognitionScreenState { + override val microphonePermissionState: MutableState = mutableStateOf(false) + override val onPermissionChange: () -> Unit = { + microphonePermissionState.value = !microphonePermissionState.value + } + } + + rule.setContent { SpeechRecognitionScreen(state) } + rule.onNodeWithText(PermissionDenied).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + rule.onNodeWithText(GrantText).assertExists().performClick() + rule.onNodeWithText(PermissionGranted).assertExists() + + } +} diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 2de3697f3..227376a01 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -4,7 +4,14 @@ - + + + + + + + Unit +) { if (!microPermissionState.hasPermission) { microPermissionState.launchPermissionRequest() + onPermissionChange() } } @Composable private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean = false) { - val text = if (hasPermission) "Permission has been granted ! " else "Permission was NOT GRANTED !" + val text = if (hasPermission) PermissionGranted else PermissionDenied Text(text = text, textAlign = TextAlign.Center, modifier = modifier) } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt new file mode 100644 index 000000000..ead955e19 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt @@ -0,0 +1,13 @@ +package ch.epfl.sdp.mobile.ui.speech_recognition + +import androidx.compose.runtime.State +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState + +@OptIn(ExperimentalPermissionsApi::class) +interface SpeechRecognitionScreenState { + + val microphonePermissionState: State + val onPermissionChange: () -> Unit + +} \ No newline at end of file From c40fb017673b3715fbe27564294da989fe50180d Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 5 Apr 2022 14:33:51 +0200 Subject: [PATCH 12/42] Pass modifier from state screen to screen --- .../ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt index cc4c50f8a..82081dda0 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt @@ -20,5 +20,5 @@ object SpeechRecognitionScreenState : SpeechRecognitionScreenState { @Composable fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier = Modifier) { val state = SpeechRecognitionScreenState - SpeechRecognitionScreen(state) + SpeechRecognitionScreen(state, modifier) } From 9ff589e588a90deea446e321c37ea5b6e601c90c Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 5 Apr 2022 14:51:50 +0200 Subject: [PATCH 13/42] Reformat --- .../StatefulSpeechRecognitionScreenTest.kt | 4 +--- .../SpeechRecognitionScreenTest.kt | 21 +++++++++---------- .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 1 - .../SpeechRecognitionScreenState.kt | 8 +++---- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index 63ac52e64..18ded8d65 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -16,7 +16,6 @@ import ch.epfl.sdp.mobile.state.StatefulSpeechRecognitionScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore import ch.epfl.sdp.mobile.ui.speech_recognition.ListeningText -import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionDenied import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionGranted import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.flow.filterIsInstance @@ -28,8 +27,7 @@ import org.junit.Test @ExperimentalPermissionsApi class StatefulSpeechRecognitionScreenTest { - @get:Rule - val rule = createComposeRule() + @get:Rule val rule = createComposeRule() @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.CAMERA) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index 5f4646f66..a0b90241a 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -37,12 +37,12 @@ class SpeechRecognitionScreenTest { @Test fun given_defaultScreenState_when_micClicked_then_displayPermissionDialog() { val state = - object : SpeechRecognitionScreenState { - override val microphonePermissionState: MutableState = mutableStateOf(false) - override val onPermissionChange: () -> Unit = { - microphonePermissionState.value = !microphonePermissionState.value + object : SpeechRecognitionScreenState { + override val microphonePermissionState: MutableState = mutableStateOf(false) + override val onPermissionChange: () -> Unit = { + microphonePermissionState.value = !microphonePermissionState.value + } } - } rule.setContent { SpeechRecognitionScreen(state) } rule.onNodeWithText(PermissionDenied).assertExists() @@ -53,18 +53,17 @@ class SpeechRecognitionScreenTest { @Test fun given_permissionDialog_when_okClicked_then_grantPermission() { val state = - object : SpeechRecognitionScreenState { - override val microphonePermissionState: MutableState = mutableStateOf(false) - override val onPermissionChange: () -> Unit = { - microphonePermissionState.value = !microphonePermissionState.value + object : SpeechRecognitionScreenState { + override val microphonePermissionState: MutableState = mutableStateOf(false) + override val onPermissionChange: () -> Unit = { + microphonePermissionState.value = !microphonePermissionState.value + } } - } rule.setContent { SpeechRecognitionScreen(state) } rule.onNodeWithText(PermissionDenied).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() rule.onNodeWithText(GrantText).assertExists().performClick() rule.onNodeWithText(PermissionGranted).assertExists() - } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 2d91df231..409fdfb02 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -13,7 +13,6 @@ import androidx.navigation.compose.rememberNavController import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.ui.home.HomeScaffold import ch.epfl.sdp.mobile.ui.home.HomeSection -import ch.epfl.sdp.mobile.ui.speech_recognition.SpeechRecognitionScreen /** The route associated to the social tab. */ private const val SocialRoute = "social" diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt index ead955e19..57afc905b 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt @@ -2,12 +2,10 @@ package ch.epfl.sdp.mobile.ui.speech_recognition import androidx.compose.runtime.State import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState @OptIn(ExperimentalPermissionsApi::class) interface SpeechRecognitionScreenState { - val microphonePermissionState: State - val onPermissionChange: () -> Unit - -} \ No newline at end of file + val microphonePermissionState: State + val onPermissionChange: () -> Unit +} From 36fd0394853fb864c25e83883e8ce47bbd39ae79 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 02:55:09 +0200 Subject: [PATCH 14/42] Add tests, doc and refactor - Add mutator - Extract permission in state - Extract strings --- .../StatefulSpeechRecognitionScreenTest.kt | 9 ++- .../SpeechRecognitionScreenTest.kt | 68 +++++++++--------- .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 4 +- .../state/StatefulSpeechRecognitionScreen.kt | 30 +++++--- .../SpeechRecognitionScreen.kt | 72 ++++++++++++++----- .../SpeechRecognitionScreenState.kt | 7 +- 6 files changed, 125 insertions(+), 65 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index 18ded8d65..457267e3f 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -15,7 +15,9 @@ import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulSpeechRecognitionScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.ui.speech_recognition.DefaultText import ch.epfl.sdp.mobile.ui.speech_recognition.ListeningText +import ch.epfl.sdp.mobile.ui.speech_recognition.MicroIconDescription import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionGranted import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.flow.filterIsInstance @@ -46,10 +48,11 @@ class StatefulSpeechRecognitionScreenTest { ProvideFacades(facade, social, chess) { StatefulSpeechRecognitionScreen(user) } } - // rule.onNodeWithText(PermissionDenied).assertExists() rule.onNodeWithText(PermissionGranted).assertExists() - rule.onNodeWithContentDescription("micro").assertExists().performClick() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() rule.onNodeWithText(ListeningText).assertExists() - rule.onNodeWithContentDescription("micro").assertExists().performClick() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + rule.onNodeWithText(DefaultText).assertExists() + } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index a0b90241a..1feaf552e 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -1,69 +1,69 @@ package ch.epfl.sdp.mobile.test.ui.speech_recognition -import androidx.compose.runtime.MutableState +import android.Manifest import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.test.rule.GrantPermissionRule +import ch.epfl.sdp.mobile.state.DefaultSpeechRecognitionScreenState import ch.epfl.sdp.mobile.ui.speech_recognition.* import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import io.mockk.every +import io.mockk.mockk import org.junit.Rule import org.junit.Test -private const val GrantText = "ALLOW" -private const val DenyText = "DENY" - @ExperimentalPermissionsApi class SpeechRecognitionScreenTest { + @get:Rule val rule = createComposeRule() + @get:Rule + val permissionRule: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) @Test fun given_defaultScreenState_when_showed_then_displayedDefaultScreen() { - val state = - object : SpeechRecognitionScreenState { - override val microphonePermissionState: MutableState = mutableStateOf(false) - override val onPermissionChange: () -> Unit = { - microphonePermissionState.value = !microphonePermissionState.value - } - } - + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns false + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) rule.setContent { SpeechRecognitionScreen(state) } - rule.onNodeWithText(PermissionDenied).assertDoesNotExist() + //Permission is granted by default :( + rule.onNodeWithText(PermissionDenied).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists() rule.onNodeWithText(DefaultText).assertExists() + } @Test - fun given_defaultScreenState_when_micClicked_then_displayPermissionDialog() { - val state = - object : SpeechRecognitionScreenState { - override val microphonePermissionState: MutableState = mutableStateOf(false) - override val onPermissionChange: () -> Unit = { - microphonePermissionState.value = !microphonePermissionState.value - } - } + fun given_defaultScreenState_when_micClicked_then_ListeningDisplayed() { + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns true + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) rule.setContent { SpeechRecognitionScreen(state) } - rule.onNodeWithText(PermissionDenied).assertExists() + rule.onNodeWithText(PermissionGranted).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(GrantText).assertExists() + rule.onNodeWithText(ListeningText).assertExists() } @Test - fun given_permissionDialog_when_okClicked_then_grantPermission() { - val state = - object : SpeechRecognitionScreenState { - override val microphonePermissionState: MutableState = mutableStateOf(false) - override val onPermissionChange: () -> Unit = { - microphonePermissionState.value = !microphonePermissionState.value - } - } + fun given_listeningMic_when_micClicked_then_stopListening() { + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns true + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) + rule.setContent { SpeechRecognitionScreen(state) } - rule.onNodeWithText(PermissionDenied).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(GrantText).assertExists().performClick() rule.onNodeWithText(PermissionGranted).assertExists() + rule.onNodeWithText(DefaultText).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + rule.onNodeWithText(ListeningText).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + + rule.onNodeWithText(ListeningText).assertDoesNotExist() + rule.onNodeWithText(DefaultText).assertExists() } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 409fdfb02..973973f0a 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -13,6 +13,7 @@ import androidx.navigation.compose.rememberNavController import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.ui.home.HomeScaffold import ch.epfl.sdp.mobile.ui.home.HomeSection +import com.google.accompanist.permissions.ExperimentalPermissionsApi /** The route associated to the social tab. */ private const val SocialRoute = "social" @@ -52,6 +53,7 @@ private const val ArRoute = "ar" * @param modifier the [Modifier] for this composable. * @param controller the [NavHostController] used to control the current destination. */ +@OptIn(ExperimentalPermissionsApi::class) @Composable fun StatefulHome( user: AuthenticatedUser, @@ -104,7 +106,7 @@ fun StatefulHome( ) } composable(ArRoute) { StatefulArScreen(Modifier.fillMaxSize()) } - composable(SpeechRecognitionRoute) { StatefulPrepareGameScreen(user, Modifier.fillMaxSize()) } + composable(SpeechRecognitionRoute) { StatefulSpeechRecognitionScreen(user, Modifier.fillMaxSize()) } } } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt index 82081dda0..a370e6b0b 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt @@ -1,24 +1,38 @@ package ch.epfl.sdp.mobile.state -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import android.Manifest +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.ui.speech_recognition.SpeechRecognitionScreen import ch.epfl.sdp.mobile.ui.speech_recognition.SpeechRecognitionScreenState import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.rememberPermissionState @ExperimentalPermissionsApi -object SpeechRecognitionScreenState : SpeechRecognitionScreenState { - override var microphonePermissionState = mutableStateOf(false) - override val onPermissionChange = { - microphonePermissionState.value = !microphonePermissionState.value +class DefaultSpeechRecognitionScreenState( + override val permissionState: PermissionState, + private val microphonePermissionState: MutableState +) : SpeechRecognitionScreenState { + + override var hasMicrophonePermission by microphonePermissionState + override fun onPermissionChange() { + hasMicrophonePermission = permissionState.hasPermission + } } @ExperimentalPermissionsApi @Composable fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier = Modifier) { - val state = SpeechRecognitionScreenState - SpeechRecognitionScreen(state, modifier) + + val permissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) + val microphonePermissionState = remember(permissionState.hasPermission) { mutableStateOf(permissionState.hasPermission) } + + val state = remember(permissionState, microphonePermissionState) { + DefaultSpeechRecognitionScreenState(permissionState, microphonePermissionState) + } + + SpeechRecognitionScreen(state = state, modifier) } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index fff9cc64f..362857eef 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -1,12 +1,13 @@ package ch.epfl.sdp.mobile.ui.speech_recognition -import android.Manifest import android.content.Context import android.content.Intent import android.os.Bundle import android.speech.RecognitionListener import android.speech.RecognizerIntent import android.speech.SpeechRecognizer +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.shape.CircleShape @@ -22,11 +23,11 @@ import androidx.compose.ui.unit.dp import ch.epfl.sdp.mobile.ui.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.rememberPermissionState import kotlin.coroutines.resume import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +/* Extracted strings used for test, may be removed later */ private const val Lang = "en-US" private const val MaxResultsCount = 10 const val PermissionGranted = "Permission has been granted ! " @@ -35,13 +36,19 @@ const val DefaultText = "---" const val ListeningText = "Listening..." const val MicroIconDescription = "micro" +/** Mutator used for cancelling a concurrent speech if the user mutes the mic */ +private val mutex = MutatorMutex() + +/** + * Screen for demonstrating the SpeechRecognition android feature + * @param state State of the screen + * @param modifier [Modifier] of this composable + */ @Composable @OptIn(ExperimentalPermissionsApi::class) fun SpeechRecognitionScreen( state: SpeechRecognitionScreenState, modifier: Modifier = Modifier, - microPermissionState: PermissionState = - rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) ) { val context = LocalContext.current @@ -51,6 +58,22 @@ fun SpeechRecognitionScreen( val microIcon = if (activeSpeech) PawniesIcons.GameMicOn else PawniesIcons.GameMicOff + /** + * Blocking function to ensure that only the most recent call to the block function is executed + * and older executions are cancelled + * + * @param block: a suspending function namely the speech recognition routine + */ + suspend fun vocalize(block: suspend () -> Unit) { + mutex.mutate(MutatePriority.UserInput) { + try { + block() + } finally { + activeSpeech = false + } + } + } + Column( verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, @@ -58,31 +81,36 @@ fun SpeechRecognitionScreen( // Speech Text Text(text, textAlign = TextAlign.Center) - // Microphone Button OutlinedButton( shape = CircleShape, onClick = { - askForPermission(microPermissionState, state.onPermissionChange) - activeSpeech = !activeSpeech && microPermissionState.hasPermission + askForPermission(state.permissionState, state::onPermissionChange) + activeSpeech = !activeSpeech && state.hasMicrophonePermission scope.launch { - if (activeSpeech) { - text = ListeningText - text = recognition(context).joinToString(separator = "\n") - activeSpeech = false - } else { - text = DefaultText + vocalize { + if (activeSpeech) { + text = ListeningText + text = recognition(context).joinToString(separator = "\n") + } else { + text = DefaultText + } } } }) { Icon(microIcon, MicroIconDescription) } // Display information about vocal permission PermissionText( - hasPermission = microPermissionState.hasPermission, + hasPermission = state.hasMicrophonePermission, ) } } +/** + * Asks the user to grant permission to use the mic if not already granted + * @param microPermissionState permission state of microphone + * @param onPermissionChange call back to change permission un the state + */ @OptIn(ExperimentalPermissionsApi::class) private fun askForPermission( microPermissionState: PermissionState, @@ -94,13 +122,22 @@ private fun askForPermission( } } +/** + * Composable responsible for displaying the permission text + * @param hasPermission True if the permission was granted false otherwise + * @param modifier [Modifier] for this composable + */ @Composable private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean = false) { val text = if (hasPermission) PermissionGranted else PermissionDenied Text(text = text, textAlign = TextAlign.Center, modifier = modifier) } -// Returns speech result from the recognizer +/** + * Returns speech results from the speech recognizer + * @param context [Context] context of the app execution + * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings + */ suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> val recognizer = SpeechRecognizer.createSpeechRecognizer(context) val speechRecognizerIntent = @@ -114,7 +151,7 @@ suspend fun recognition(context: Context): List = suspendCancellableCoro override fun onResults(results: Bundle?) { super.onResults(results) cont.resume( - // results cannot br null + // results cannot be null results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) } } @@ -128,6 +165,9 @@ suspend fun recognition(context: Context): List = suspendCancellableCoro } } +/** + * Adapter class for Recognition' listener + */ abstract class RecognitionListenerAdapter : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) = Unit override fun onBeginningOfSpeech() = Unit diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt index 57afc905b..4d504b4c8 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt @@ -1,11 +1,12 @@ package ch.epfl.sdp.mobile.ui.speech_recognition -import androidx.compose.runtime.State import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState @OptIn(ExperimentalPermissionsApi::class) interface SpeechRecognitionScreenState { - val microphonePermissionState: State - val onPermissionChange: () -> Unit + val permissionState: PermissionState + val hasMicrophonePermission: Boolean + fun onPermissionChange(): Unit } From 40031fc58957bc5fde865a5c7a745e64b2aa7aaa Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 02:59:28 +0200 Subject: [PATCH 15/42] Format --- .../test/state/StatefulSpeechRecognitionScreenTest.kt | 1 - .../speech_recognition/SpeechRecognitionScreenTest.kt | 10 ++++------ .../java/ch/epfl/sdp/mobile/state/StatefulHome.kt | 4 +++- .../mobile/state/StatefulSpeechRecognitionScreen.kt | 11 ++++++----- .../ui/speech_recognition/SpeechRecognitionScreen.kt | 10 ++++------ 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index 457267e3f..706a23048 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -53,6 +53,5 @@ class StatefulSpeechRecognitionScreenTest { rule.onNodeWithText(ListeningText).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() rule.onNodeWithText(DefaultText).assertExists() - } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index 1feaf552e..adcc6092c 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -28,20 +28,19 @@ class SpeechRecognitionScreenTest { fun given_defaultScreenState_when_showed_then_displayedDefaultScreen() { val mockedPermission = mockk() every { mockedPermission.hasPermission } returns false - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) rule.setContent { SpeechRecognitionScreen(state) } - //Permission is granted by default :( + // Permission is granted by default :( rule.onNodeWithText(PermissionDenied).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists() rule.onNodeWithText(DefaultText).assertExists() - } @Test fun given_defaultScreenState_when_micClicked_then_ListeningDisplayed() { val mockedPermission = mockk() every { mockedPermission.hasPermission } returns true - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) rule.setContent { SpeechRecognitionScreen(state) } rule.onNodeWithText(PermissionGranted).assertExists() @@ -53,8 +52,7 @@ class SpeechRecognitionScreenTest { fun given_listeningMic_when_micClicked_then_stopListening() { val mockedPermission = mockk() every { mockedPermission.hasPermission } returns true - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) - + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) rule.setContent { SpeechRecognitionScreen(state) } rule.onNodeWithText(PermissionGranted).assertExists() diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 973973f0a..97a8467e6 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -106,7 +106,9 @@ fun StatefulHome( ) } composable(ArRoute) { StatefulArScreen(Modifier.fillMaxSize()) } - composable(SpeechRecognitionRoute) { StatefulSpeechRecognitionScreen(user, Modifier.fillMaxSize()) } + composable(SpeechRecognitionRoute) { + StatefulSpeechRecognitionScreen(user, Modifier.fillMaxSize()) + } } } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt index a370e6b0b..bad16b482 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt @@ -19,7 +19,6 @@ class DefaultSpeechRecognitionScreenState( override var hasMicrophonePermission by microphonePermissionState override fun onPermissionChange() { hasMicrophonePermission = permissionState.hasPermission - } } @@ -28,11 +27,13 @@ class DefaultSpeechRecognitionScreenState( fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier = Modifier) { val permissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) - val microphonePermissionState = remember(permissionState.hasPermission) { mutableStateOf(permissionState.hasPermission) } + val microphonePermissionState = + remember(permissionState.hasPermission) { mutableStateOf(permissionState.hasPermission) } - val state = remember(permissionState, microphonePermissionState) { - DefaultSpeechRecognitionScreenState(permissionState, microphonePermissionState) - } + val state = + remember(permissionState, microphonePermissionState) { + DefaultSpeechRecognitionScreenState(permissionState, microphonePermissionState) + } SpeechRecognitionScreen(state = state, modifier) } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 362857eef..9a419878f 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -134,9 +134,9 @@ private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean } /** - * Returns speech results from the speech recognizer - * @param context [Context] context of the app execution - * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings + * Returns speech results from the speech recognizer + * @param context [Context] context of the app execution + * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings */ suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> val recognizer = SpeechRecognizer.createSpeechRecognizer(context) @@ -165,9 +165,7 @@ suspend fun recognition(context: Context): List = suspendCancellableCoro } } -/** - * Adapter class for Recognition' listener - */ +/** Adapter class for Recognition' listener */ abstract class RecognitionListenerAdapter : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) = Unit override fun onBeginningOfSpeech() = Unit From 1fc3521a20ce689ab63ffa28e8a141c8e5dd1cd3 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 03:33:02 +0200 Subject: [PATCH 16/42] Fix test by changing permission to Record audio --- .../mobile/test/state/StatefulSpeechRecognitionScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index 706a23048..fba29dc22 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -31,7 +31,7 @@ class StatefulSpeechRecognitionScreenTest { @get:Rule val rule = createComposeRule() - @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.CAMERA) + @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.RECORD_AUDIO) @Test fun given_defaultScreen_when_micClicked_and_okClicked_then_permissionGranted() = runTest { From f302b302df5de208e251a93a54713ba55891e436 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 04:09:28 +0200 Subject: [PATCH 17/42] Refresh CI From 6edbe7ea1f917375b276338a1e4d3c375ea8d8c2 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:36:06 +0200 Subject: [PATCH 18/42] Add more tests to increase coverage --- .../SpeechRecognitionScreenTest.kt | 22 ++++++- .../state/StatefulSpeechRecognitionScreen.kt | 12 +++- .../SpeechRecognitionScreen.kt | 58 +----------------- .../SpeechRecognitionScreenState.kt | 8 +++ .../speech_recognition/SpeechRecognizable.kt | 13 ++++ .../SpeechRecognizerEntity.kt | 61 +++++++++++++++++++ 6 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index adcc6092c..2eab65120 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -11,6 +11,7 @@ import ch.epfl.sdp.mobile.state.DefaultSpeechRecognitionScreenState import ch.epfl.sdp.mobile.ui.speech_recognition.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import org.junit.Rule @@ -18,7 +19,6 @@ import org.junit.Test @ExperimentalPermissionsApi class SpeechRecognitionScreenTest { - @get:Rule val rule = createComposeRule() @get:Rule val permissionRule: GrantPermissionRule = @@ -64,4 +64,24 @@ class SpeechRecognitionScreenTest { rule.onNodeWithText(ListeningText).assertDoesNotExist() rule.onNodeWithText(DefaultText).assertExists() } + + @Test + fun given_listeningMic_when_talking_then_textDisplayed() { + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns true + + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) + + val speech = "Hello World" + + val mockedRecognizer: SpeechRecognizable = mockk() + + coEvery { mockedRecognizer.recognition(any()) } returns listOf(speech) + + rule.setContent { SpeechRecognitionScreen(state, mockedRecognizer) } + rule.onNodeWithText(PermissionGranted).assertExists() + rule.onNodeWithText(DefaultText).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + rule.onNodeWithText(speech).assertExists() + } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt index bad16b482..24ae8f397 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt @@ -10,6 +10,11 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.rememberPermissionState +/** + * Default implementation of the [SpeechRecognitionScreenState] + * @property permissionState [PermissionState] for launching permission dialog + * @property microphonePermissionState mutable state of microphone permission + */ @ExperimentalPermissionsApi class DefaultSpeechRecognitionScreenState( override val permissionState: PermissionState, @@ -22,6 +27,11 @@ class DefaultSpeechRecognitionScreenState( } } +/** + * Stateful composable for the SpeechRecognitionScreen + * @param user authenticated user + * @param modifier [Modifier] of this composable + */ @ExperimentalPermissionsApi @Composable fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier = Modifier) { @@ -35,5 +45,5 @@ fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier DefaultSpeechRecognitionScreenState(permissionState, microphonePermissionState) } - SpeechRecognitionScreen(state = state, modifier) + SpeechRecognitionScreen(state = state, modifier = modifier) } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 9a419878f..cf3787894 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -1,11 +1,5 @@ package ch.epfl.sdp.mobile.ui.speech_recognition -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.speech.RecognitionListener -import android.speech.RecognizerIntent -import android.speech.SpeechRecognizer import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.Arrangement @@ -23,13 +17,9 @@ import androidx.compose.ui.unit.dp import ch.epfl.sdp.mobile.ui.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState -import kotlin.coroutines.resume import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine /* Extracted strings used for test, may be removed later */ -private const val Lang = "en-US" -private const val MaxResultsCount = 10 const val PermissionGranted = "Permission has been granted ! " const val PermissionDenied = "Permission was NOT GRANTED !" const val DefaultText = "---" @@ -48,6 +38,7 @@ private val mutex = MutatorMutex() @OptIn(ExperimentalPermissionsApi::class) fun SpeechRecognitionScreen( state: SpeechRecognitionScreenState, + recognizer: SpeechRecognizable = SpeechRecognizerEntity(), modifier: Modifier = Modifier, ) { @@ -91,7 +82,7 @@ fun SpeechRecognitionScreen( vocalize { if (activeSpeech) { text = ListeningText - text = recognition(context).joinToString(separator = "\n") + text = recognizer.recognition(context).joinToString(separator = "\n") } else { text = DefaultText } @@ -132,48 +123,3 @@ private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean val text = if (hasPermission) PermissionGranted else PermissionDenied Text(text = text, textAlign = TextAlign.Center, modifier = modifier) } - -/** - * Returns speech results from the speech recognizer - * @param context [Context] context of the app execution - * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings - */ -suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> - val recognizer = SpeechRecognizer.createSpeechRecognizer(context) - val speechRecognizerIntent = - Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action - .putExtra(RecognizerIntent.EXTRA_LANGUAGE, Lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, MaxResultsCount) // Number of results - - // Listener for results - val listener = - object : RecognitionListenerAdapter() { - override fun onResults(results: Bundle?) { - super.onResults(results) - cont.resume( - // results cannot be null - results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) - } - } - recognizer.setRecognitionListener(listener) - recognizer.startListening(speechRecognizerIntent) - - // Clearing upon coroutine cancellation - cont.invokeOnCancellation { - recognizer.stopListening() - recognizer.destroy() - } -} - -/** Adapter class for Recognition' listener */ -abstract class RecognitionListenerAdapter : RecognitionListener { - override fun onReadyForSpeech(params: Bundle?) = Unit - override fun onBeginningOfSpeech() = Unit - override fun onRmsChanged(rmsdB: Float) = Unit - override fun onBufferReceived(buffer: ByteArray?) = Unit - override fun onEndOfSpeech() = Unit - override fun onError(error: Int) = Unit - override fun onResults(results: Bundle?) = Unit - override fun onPartialResults(partialResults: Bundle?) = Unit - override fun onEvent(eventType: Int, params: Bundle?) = Unit -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt index 4d504b4c8..46121ef48 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt @@ -1,12 +1,20 @@ package ch.epfl.sdp.mobile.ui.speech_recognition +import androidx.compose.runtime.Stable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState +/** State of the SpeechRecognition screen */ @OptIn(ExperimentalPermissionsApi::class) +@Stable interface SpeechRecognitionScreenState { + /* PermissionState for microphone access */ val permissionState: PermissionState + + /* Microphone right access */ val hasMicrophonePermission: Boolean + + /* Call back when permission is changed */ fun onPermissionChange(): Unit } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt new file mode 100644 index 000000000..64c863919 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt @@ -0,0 +1,13 @@ +package ch.epfl.sdp.mobile.ui.speech_recognition + +import android.content.Context + +/** Interface ued to extract speech recognition routine */ +interface SpeechRecognizable { + /** + * Suspending function that returns list of speech + * @param context [Context] of app execution + * @return speech candidates + */ + suspend fun recognition(context: Context): List +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt new file mode 100644 index 000000000..a2f0097f9 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt @@ -0,0 +1,61 @@ +package ch.epfl.sdp.mobile.ui.speech_recognition + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.speech.RecognitionListener +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +class SpeechRecognizerEntity( + private val lang: String = "en-US", + private val maxResultsCount: Int = 10 +) : SpeechRecognizable { + /** + * Returns speech results from the speech recognizer + * @param context [Context] context of the app execution + * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings + */ + override suspend fun recognition(context: Context): List = + suspendCancellableCoroutine { cont -> + val recognizer = SpeechRecognizer.createSpeechRecognizer(context) + val speechRecognizerIntent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action + .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results + + // Listener for results + val listener = + object : RecognitionListenerAdapter() { + override fun onResults(results: Bundle?) { + super.onResults(results) + cont.resume( + // results cannot be null + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) + } + } + recognizer.setRecognitionListener(listener) + recognizer.startListening(speechRecognizerIntent) + + // Clearing upon coroutine cancellation + cont.invokeOnCancellation { + recognizer.stopListening() + recognizer.destroy() + } + } + + /** Adapter class for Recognition' listener */ + abstract class RecognitionListenerAdapter : RecognitionListener { + override fun onReadyForSpeech(params: Bundle?) = Unit + override fun onBeginningOfSpeech() = Unit + override fun onRmsChanged(rmsdB: Float) = Unit + override fun onBufferReceived(buffer: ByteArray?) = Unit + override fun onEndOfSpeech() = Unit + override fun onError(error: Int) = Unit + override fun onResults(results: Bundle?) = Unit + override fun onPartialResults(partialResults: Bundle?) = Unit + override fun onEvent(eventType: Int, params: Bundle?) = Unit + } +} From 2246da65caef88a3ea78f778497faa65740f42bc Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:43:57 +0200 Subject: [PATCH 19/42] Refresh Cysrus From 87ca37b5696e44d5483f47e6c7fd04b9810e4fb9 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:26:44 +0200 Subject: [PATCH 20/42] Refactor code and make classes testable --- .../StatefulSpeechRecognitionScreenTest.kt | 2 +- .../SpeechRecognitionScreenTest.kt | 53 ++++++++++++++++++- .../SpeechRecognitionScreen.kt | 3 +- .../SpeechRecognizerEntity.kt | 30 ++++++----- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index fba29dc22..21b9d0d42 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -34,7 +34,7 @@ class StatefulSpeechRecognitionScreenTest { @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.RECORD_AUDIO) @Test - fun given_defaultScreen_when_micClicked_and_okClicked_then_permissionGranted() = runTest { + fun given_defaultScreen_when_micActivatedAndDeactivated_then_defaultState() = runTest { val auth = emptyAuth() val store = emptyStore() val facade = AuthenticationFacade(auth, store) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index 2eab65120..e360a8528 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -1,13 +1,25 @@ package ch.epfl.sdp.mobile.test.ui.speech_recognition import android.Manifest +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import android.util.Log import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import ch.epfl.sdp.mobile.state.DefaultSpeechRecognitionScreenState +import ch.epfl.sdp.mobile.state.HomeActivity import ch.epfl.sdp.mobile.ui.speech_recognition.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState @@ -19,7 +31,9 @@ import org.junit.Test @ExperimentalPermissionsApi class SpeechRecognitionScreenTest { + @get:Rule val rule = createComposeRule() + @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) @@ -75,13 +89,48 @@ class SpeechRecognitionScreenTest { val speech = "Hello World" val mockedRecognizer: SpeechRecognizable = mockk() - coEvery { mockedRecognizer.recognition(any()) } returns listOf(speech) - rule.setContent { SpeechRecognitionScreen(state, mockedRecognizer) } + rule.setContent { SpeechRecognitionScreen(state, recognizer = mockedRecognizer) } rule.onNodeWithText(PermissionGranted).assertExists() rule.onNodeWithText(DefaultText).assertExists() rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() rule.onNodeWithText(speech).assertExists() } + + @get:Rule val androidRule = createAndroidComposeRule() + + @Test + fun test() { + + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns true + + // Initiate Espresso intents listening + try { + Intents.init() + + // Rule sets content to test + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) + androidRule.setContent { SpeechRecognitionScreen(state) } + + // Mock result intent + val speech = "Hello World" + val resultData = + Intent(InstrumentationRegistry.getInstrumentation().context, HomeActivity::class.java) + + resultData.putExtra(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(speech)) + + val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) + intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) + androidRule.onNodeWithText(DefaultText).assertExists() + androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + androidRule.onNodeWithText(ListeningText).assertExists() + + Log.d("tag", "all intents ${Intents.getIntents()}") + androidRule.onNodeWithText(speech).assertExists(speech) + } finally { + Intents.release() + } + } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index cf3787894..217a44286 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -32,14 +32,15 @@ private val mutex = MutatorMutex() /** * Screen for demonstrating the SpeechRecognition android feature * @param state State of the screen + * @param recognizer [SpeechRecognizerEntity] entity used in speech recognition for this screen * @param modifier [Modifier] of this composable */ @Composable @OptIn(ExperimentalPermissionsApi::class) fun SpeechRecognitionScreen( state: SpeechRecognitionScreenState, - recognizer: SpeechRecognizable = SpeechRecognizerEntity(), modifier: Modifier = Modifier, + recognizer: SpeechRecognizable = SpeechRecognizerEntity(), ) { val context = LocalContext.current diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt index a2f0097f9..3f6bbe376 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt @@ -7,16 +7,30 @@ import android.speech.RecognitionListener import android.speech.RecognizerIntent import android.speech.SpeechRecognizer import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine +val defaultListener: (CancellableContinuation>) -> RecognitionListener = { cont -> + object : SpeechRecognizerEntity.RecognitionListenerAdapter() { + override fun onResults(results: Bundle?) { + super.onResults(results) + cont.resume( + // results cannot be null + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) + } + } +} + class SpeechRecognizerEntity( private val lang: String = "en-US", - private val maxResultsCount: Int = 10 + private val maxResultsCount: Int = 10, + private val listener: CancellableContinuation>.() -> RecognitionListener = + defaultListener ) : SpeechRecognizable { /** * Returns speech results from the speech recognizer * @param context [Context] context of the app execution - * @return List of size maximum [MaxResultsCount] of speech recognizer results as strings + * @return List of size maximum [maxResultsCount] of speech recognizer results as strings */ override suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> @@ -25,18 +39,10 @@ class SpeechRecognizerEntity( Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results + .putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 1000) // Listener for results - val listener = - object : RecognitionListenerAdapter() { - override fun onResults(results: Bundle?) { - super.onResults(results) - cont.resume( - // results cannot be null - results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) - } - } - recognizer.setRecognitionListener(listener) + recognizer.setRecognitionListener(listener(cont)) recognizer.startListening(speechRecognizerIntent) // Clearing upon coroutine cancellation From 9f11201ab96b75ae68d57fbf5975e302b5f6e405 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:33:16 +0200 Subject: [PATCH 21/42] Add test comments --- .../test/ui/speech_recognition/SpeechRecognitionScreenTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index e360a8528..191c307b7 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -125,8 +125,9 @@ class SpeechRecognitionScreenTest { intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) androidRule.onNodeWithText(DefaultText).assertExists() androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - androidRule.onNodeWithText(ListeningText).assertExists() + //TODO: not intents are captured by test + // Possible bug -> recognizer keeps listening and do not send intents Log.d("tag", "all intents ${Intents.getIntents()}") androidRule.onNodeWithText(speech).assertExists(speech) } finally { From 93c62d556babf20f7ea4d649cc174273430eacbe Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:44:56 +0200 Subject: [PATCH 22/42] Remove extra from intent --- .../sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt index 3f6bbe376..7a47c9f99 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt @@ -39,7 +39,6 @@ class SpeechRecognizerEntity( Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results - .putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 1000) // Listener for results recognizer.setRecognitionListener(listener(cont)) From 1ac978c5e06b1f078386d3decec5ca91f016aa23 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:50:11 +0200 Subject: [PATCH 23/42] Add broken intent test with comments --- .../SpeechRecognitionScreenTest.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index 191c307b7..c9a56837a 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -98,16 +98,17 @@ class SpeechRecognitionScreenTest { rule.onNodeWithText(speech).assertExists() } + //Start activity @get:Rule val androidRule = createAndroidComposeRule() @Test - fun test() { + fun brokenIntentsTest() { val mockedPermission = mockk() every { mockedPermission.hasPermission } returns true - // Initiate Espresso intents listening try { + //Initiate intent capturing Intents.init() // Rule sets content to test @@ -122,15 +123,27 @@ class SpeechRecognitionScreenTest { resultData.putExtra(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(speech)) val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) + + //Stub intent responses of the recognizer intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) + + androidRule.onNodeWithText(DefaultText).assertExists() + + //Start listen for result but never stop androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() + androidRule.onNodeWithContentDescription(ListeningText).assertExists() - //TODO: not intents are captured by test + // TODO: no intents are captured by test // Possible bug -> recognizer keeps listening and do not send intents - Log.d("tag", "all intents ${Intents.getIntents()}") + + Log.d("tag", "all intents ${Intents.getIntents()}") // No intents are seent by the HomeActivity Class + // The recognizer keeps listening + + //This fails as no response is sent back to the HomeActivity androidRule.onNodeWithText(speech).assertExists(speech) } finally { + //Stop intent capturing Intents.release() } } From 8e577de9b20838726638d8696d0d2b3f41fe7ae8 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 3 May 2022 11:10:27 +0000 Subject: [PATCH 24/42] Add unstable tests --- .../SpeechRecognitionScreenTest.kt | 136 +++++++++++------- .../SpeechRecognitionScreen.kt | 2 +- .../SpeechRecognizerEntity.kt | 55 +++---- 3 files changed, 117 insertions(+), 76 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index c9a56837a..ccac5b69e 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -1,31 +1,26 @@ package ch.epfl.sdp.mobile.test.ui.speech_recognition import android.Manifest -import android.app.Activity -import android.app.Instrumentation -import android.content.Intent -import android.speech.RecognizerIntent -import android.speech.SpeechRecognizer +import android.os.Bundle +import android.speech.RecognitionListener import android.util.Log import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intending -import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import ch.epfl.sdp.mobile.state.DefaultSpeechRecognitionScreenState -import ch.epfl.sdp.mobile.state.HomeActivity import ch.epfl.sdp.mobile.ui.speech_recognition.* import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.test.runTest +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual import org.junit.Rule import org.junit.Test @@ -34,6 +29,17 @@ class SpeechRecognitionScreenTest { @get:Rule val rule = createComposeRule() + @Test fun given_noGrantedPermission_when_screenDisplayed_then_noPermissionText(){ + val mockedPermission = mockk() + every { mockedPermission.hasPermission } returns false + val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) + rule.setContent { SpeechRecognitionScreen(state) } + + rule.onNodeWithText(PermissionDenied).assertExists() + rule.onNodeWithContentDescription(MicroIconDescription).assertExists() + rule.onNodeWithText(DefaultText).assertExists() + } + @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) @@ -99,52 +105,82 @@ class SpeechRecognitionScreenTest { } //Start activity - @get:Rule val androidRule = createAndroidComposeRule() - - @Test - fun brokenIntentsTest() { - - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns true - - try { - //Initiate intent capturing - Intents.init() - - // Rule sets content to test - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) - androidRule.setContent { SpeechRecognitionScreen(state) } - - // Mock result intent - val speech = "Hello World" - val resultData = - Intent(InstrumentationRegistry.getInstrumentation().context, HomeActivity::class.java) - - resultData.putExtra(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(speech)) +// @get:Rule val androidRule = createAndroidComposeRule() +// +// @Test +// fun brokenIntentsTest() { +// +// val mockedPermission = mockk() +// every { mockedPermission.hasPermission } returns true +// +// try { +// //Initiate intent capturing +// Intents.init() +// +// // Rule sets content to test +// val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) +// androidRule.setContent{ SpeechRecognitionScreen(state) } +// +// +// // Mock result intent +// val speech = "Hello World" +// val resultData = Intent(androidRule.activity.applicationContext, HomeActivity::class.java) +// resultData.putExtra(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(speech)) +// +// val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) +// +// //Stub intent responses of the recognizer +// intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) +// +// androidRule.onNodeWithText(DefaultText).assertExists() +// +// //Start listen for result but never stop +// androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() +// // androidRule.onNodeWithContentDescription(ListeningText).assertExists() +// +// // TODO: no intents are captured by test +// // Possible bug -> recognizer keeps listening and do not send intents +// +// Log.d("tag", "all intents ${Intents.getIntents()}") // No intents are sent by the HomeActivity Class +// // The recognizer keeps listening +// +// //This fails as no response is sent back to the HomeActivity +// androidRule.onNodeWithText(speech).assertExists(speech) +// } finally { +// //Stop intent capturing +// Intents.release() +// } +// } + + @Test fun testListener() = runTest{ + val testResults = arrayListOf("Hello", "World") + val listenerResults = suspendCancellableCoroutine> { cont -> + val bundle : Bundle = mockk() + every { bundle.getStringArrayList(any())} returns testResults + defaultListener(cont).onResults(bundle) + cont.invokeOnCancellation { + Log.d("TAGG", "Canceling...") + assertThat(1, IsEqual(1)) + } + cont.cancel() + } - val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) + assertThat(listenerResults,IsEqual(testResults)) - //Stub intent responses of the recognizer - intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) - androidRule.onNodeWithText(DefaultText).assertExists() - //Start listen for result but never stop - androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - androidRule.onNodeWithContentDescription(ListeningText).assertExists() + } - // TODO: no intents are captured by test - // Possible bug -> recognizer keeps listening and do not send intents + @Test fun testListenerEmpty() = runTest { + val listenerResults = suspendCancellableCoroutine> { cont -> + val bundle : Bundle = mockk() + every {bundle.getStringArrayList(any())} returns null + defaultListener(cont).onResults(bundle) + } - Log.d("tag", "all intents ${Intents.getIntents()}") // No intents are seent by the HomeActivity Class - // The recognizer keeps listening + Log.d("TAG", "After") + assertThat(listenerResults, IsEqual(emptyList())) - //This fails as no response is sent back to the HomeActivity - androidRule.onNodeWithText(speech).assertExists(speech) - } finally { - //Stop intent capturing - Intents.release() - } } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt index 217a44286..ca8665e89 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt @@ -19,7 +19,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import kotlinx.coroutines.launch -/* Extracted strings used for test, may be removed later */ +/* Extracted strings used for test, maybe removed later */ const val PermissionGranted = "Permission has been granted ! " const val PermissionDenied = "Permission was NOT GRANTED !" const val DefaultText = "---" diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt index 7a47c9f99..900de8a0b 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt @@ -10,21 +10,24 @@ import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine -val defaultListener: (CancellableContinuation>) -> RecognitionListener = { cont -> - object : SpeechRecognizerEntity.RecognitionListenerAdapter() { - override fun onResults(results: Bundle?) { - super.onResults(results) - cont.resume( - // results cannot be null - results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) +val defaultListener: (CancellableContinuation>) -> RecognitionListener = + { cont-> + object : SpeechRecognizerEntity.RecognitionListenerAdapter() { + override fun onResults(results: Bundle?) { + super.onResults(results) + cont.resume( + // results cannot be null + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) + } + + } } - } -} class SpeechRecognizerEntity( private val lang: String = "en-US", private val maxResultsCount: Int = 10, - private val listener: CancellableContinuation>.() -> RecognitionListener = + private val listener: + (CancellableContinuation>) -> RecognitionListener = defaultListener ) : SpeechRecognizable { /** @@ -34,22 +37,24 @@ class SpeechRecognizerEntity( */ override suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> - val recognizer = SpeechRecognizer.createSpeechRecognizer(context) - val speechRecognizerIntent = - Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action - .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results + val recognizer = SpeechRecognizer.createSpeechRecognizer(context) + val speechRecognizerIntent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action + .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results + // Listener for results + recognizer.setRecognitionListener( + listener( + cont + )) + recognizer.startListening(speechRecognizerIntent) - // Listener for results - recognizer.setRecognitionListener(listener(cont)) - recognizer.startListening(speechRecognizerIntent) - - // Clearing upon coroutine cancellation - cont.invokeOnCancellation { - recognizer.stopListening() - recognizer.destroy() - } - } + // Clearing upon coroutine cancellation + cont.invokeOnCancellation { + recognizer.stopListening() + recognizer.destroy() + } + } /** Adapter class for Recognition' listener */ abstract class RecognitionListenerAdapter : RecognitionListener { From 5b5eefae239fac22f03c3c16d1f365d5110b6376 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 3 May 2022 11:14:18 +0000 Subject: [PATCH 25/42] Refactor added tests --- .../SpeechRecognitionScreenTest.kt | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index ccac5b69e..fc9a543cd 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -1,6 +1,5 @@ package ch.epfl.sdp.mobile.test.ui.speech_recognition -import android.Manifest import android.os.Bundle import android.speech.RecognitionListener import android.util.Log @@ -104,53 +103,6 @@ class SpeechRecognitionScreenTest { rule.onNodeWithText(speech).assertExists() } - //Start activity -// @get:Rule val androidRule = createAndroidComposeRule() -// -// @Test -// fun brokenIntentsTest() { -// -// val mockedPermission = mockk() -// every { mockedPermission.hasPermission } returns true -// -// try { -// //Initiate intent capturing -// Intents.init() -// -// // Rule sets content to test -// val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) -// androidRule.setContent{ SpeechRecognitionScreen(state) } -// -// -// // Mock result intent -// val speech = "Hello World" -// val resultData = Intent(androidRule.activity.applicationContext, HomeActivity::class.java) -// resultData.putExtra(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(speech)) -// -// val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) -// -// //Stub intent responses of the recognizer -// intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)).respondWith(result) -// -// androidRule.onNodeWithText(DefaultText).assertExists() -// -// //Start listen for result but never stop -// androidRule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() -// // androidRule.onNodeWithContentDescription(ListeningText).assertExists() -// -// // TODO: no intents are captured by test -// // Possible bug -> recognizer keeps listening and do not send intents -// -// Log.d("tag", "all intents ${Intents.getIntents()}") // No intents are sent by the HomeActivity Class -// // The recognizer keeps listening -// -// //This fails as no response is sent back to the HomeActivity -// androidRule.onNodeWithText(speech).assertExists(speech) -// } finally { -// //Stop intent capturing -// Intents.release() -// } -// } @Test fun testListener() = runTest{ val testResults = arrayListOf("Hello", "World") @@ -158,18 +110,8 @@ class SpeechRecognitionScreenTest { val bundle : Bundle = mockk() every { bundle.getStringArrayList(any())} returns testResults defaultListener(cont).onResults(bundle) - cont.invokeOnCancellation { - Log.d("TAGG", "Canceling...") - assertThat(1, IsEqual(1)) - } - cont.cancel() } - assertThat(listenerResults,IsEqual(testResults)) - - - - } @Test fun testListenerEmpty() = runTest { @@ -178,9 +120,6 @@ class SpeechRecognitionScreenTest { every {bundle.getStringArrayList(any())} returns null defaultListener(cont).onResults(bundle) } - - Log.d("TAG", "After") assertThat(listenerResults, IsEqual(emptyList())) - } } From 63cadca65a2ab109f82b4a1bb1d741b904d77549 Mon Sep 17 00:00:00 2001 From: BadrTad <56789776+BadrTad@users.noreply.github.com> Date: Tue, 3 May 2022 14:03:40 +0000 Subject: [PATCH 26/42] Fix merge build --- .../SpeechRecognitionScreenTest.kt | 37 +++++++------ .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 1 + .../SpeechRecognizerEntity.kt | 54 +++++++++---------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt index fc9a543cd..163cc89bc 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt @@ -1,8 +1,7 @@ package ch.epfl.sdp.mobile.test.ui.speech_recognition +import android.Manifest import android.os.Bundle -import android.speech.RecognitionListener -import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -28,7 +27,8 @@ class SpeechRecognitionScreenTest { @get:Rule val rule = createComposeRule() - @Test fun given_noGrantedPermission_when_screenDisplayed_then_noPermissionText(){ + @Test + fun given_noGrantedPermission_when_screenDisplayed_then_noPermissionText() { val mockedPermission = mockk() every { mockedPermission.hasPermission } returns false val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) @@ -103,23 +103,26 @@ class SpeechRecognitionScreenTest { rule.onNodeWithText(speech).assertExists() } - - @Test fun testListener() = runTest{ + @Test + fun testListener() = runTest { val testResults = arrayListOf("Hello", "World") - val listenerResults = suspendCancellableCoroutine> { cont -> - val bundle : Bundle = mockk() - every { bundle.getStringArrayList(any())} returns testResults - defaultListener(cont).onResults(bundle) - } - assertThat(listenerResults,IsEqual(testResults)) + val listenerResults = + suspendCancellableCoroutine> { cont -> + val bundle: Bundle = mockk() + every { bundle.getStringArrayList(any()) } returns testResults + defaultListener(cont).onResults(bundle) + } + assertThat(listenerResults, IsEqual(testResults)) } - @Test fun testListenerEmpty() = runTest { - val listenerResults = suspendCancellableCoroutine> { cont -> - val bundle : Bundle = mockk() - every {bundle.getStringArrayList(any())} returns null - defaultListener(cont).onResults(bundle) - } + @Test + fun testListenerEmpty() = runTest { + val listenerResults = + suspendCancellableCoroutine> { cont -> + val bundle: Bundle = mockk() + every { bundle.getStringArrayList(any()) } returns null + defaultListener(cont).onResults(bundle) + } assertThat(listenerResults, IsEqual(emptyList())) } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 7ee17e5f3..c4de2adb9 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -139,6 +139,7 @@ fun StatefulHome( StatefulArScreen(user, id, Modifier.fillMaxSize()) } } + } } /** Maps a [NavBackStackEntry] to the appropriate [HomeSection]. */ diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt index 900de8a0b..b93fb8c9c 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt @@ -10,24 +10,21 @@ import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine -val defaultListener: (CancellableContinuation>) -> RecognitionListener = - { cont-> - object : SpeechRecognizerEntity.RecognitionListenerAdapter() { - override fun onResults(results: Bundle?) { - super.onResults(results) - cont.resume( - // results cannot be null - results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) - } - - } +val defaultListener: (CancellableContinuation>) -> RecognitionListener = { cont -> + object : SpeechRecognizerEntity.RecognitionListenerAdapter() { + override fun onResults(results: Bundle?) { + super.onResults(results) + cont.resume( + // results cannot be null + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) } + } +} class SpeechRecognizerEntity( private val lang: String = "en-US", private val maxResultsCount: Int = 10, - private val listener: - (CancellableContinuation>) -> RecognitionListener = + private val listener: (CancellableContinuation>) -> RecognitionListener = defaultListener ) : SpeechRecognizable { /** @@ -37,24 +34,21 @@ class SpeechRecognizerEntity( */ override suspend fun recognition(context: Context): List = suspendCancellableCoroutine { cont -> - val recognizer = SpeechRecognizer.createSpeechRecognizer(context) - val speechRecognizerIntent = - Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action - .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results - // Listener for results - recognizer.setRecognitionListener( - listener( - cont - )) - recognizer.startListening(speechRecognizerIntent) + val recognizer = SpeechRecognizer.createSpeechRecognizer(context) + val speechRecognizerIntent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action + .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language + .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results + // Listener for results + recognizer.setRecognitionListener(listener(cont)) + recognizer.startListening(speechRecognizerIntent) - // Clearing upon coroutine cancellation - cont.invokeOnCancellation { - recognizer.stopListening() - recognizer.destroy() - } - } + // Clearing upon coroutine cancellation + cont.invokeOnCancellation { + recognizer.stopListening() + recognizer.destroy() + } + } /** Adapter class for Recognition' listener */ abstract class RecognitionListenerAdapter : RecognitionListener { From acf5c89211e1a4c5305b5ff8a508a8494c4904e4 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 18:24:53 +0200 Subject: [PATCH 27/42] Introduce `SpeechFacade`, `SpeechRecognizerFactory` and `SpeechRecognizer` --- .../speech/FailingSpeechRecognizerFactory.kt | 20 ++ .../test/state/ClassicChessBoardStateTest.kt | 8 +- .../test/state/CompositionLocalsTest.kt | 8 + .../sdp/mobile/test/state/NavigationTest.kt | 14 +- .../mobile/test/state/StatefulArScreenTest.kt | 5 +- .../test/state/StatefulFollowingScreenTest.kt | 21 +- .../test/state/StatefulGameScreenTest.kt | 20 +- .../sdp/mobile/test/state/StatefulHomeTest.kt | 47 ++-- .../test/state/StatefulPlayScreenTest.kt | 223 +++++++++--------- .../state/StatefulPrepareGameScreenTest.kt | 35 ++- .../test/state/StatefulProfileScreenTest.kt | 5 +- .../test/state/StatefulSettingScreenTest.kt | 61 ++--- .../test/state/StatefulSettingsScreenTest.kt | 5 +- .../StatefulSpeechRecognitionScreenTest.kt | 5 +- .../AuthenticationScreenTest.kt | 17 +- .../mobile/application/speech/SpeechFacade.kt | 64 +++++ .../infrastructure/speech/SpeechRecognizer.kt | 35 +++ .../speech/SpeechRecognizerFactory.kt | 8 + .../android/AndroidSpeechRecognizerFactory.kt | 72 ++++++ .../android/RecognitionListenerAdapter.kt | 20 ++ .../sdp/mobile/state/CompositionLocals.kt | 6 + .../ch/epfl/sdp/mobile/state/HomeActivity.kt | 11 +- .../sdp/mobile/state/StatefulGameScreen.kt | 91 ++++++- .../ch/epfl/sdp/mobile/ui/game/GameScreen.kt | 4 + .../sdp/mobile/ui/game/GameScreenState.kt | 9 +- .../mobile/ui/game/SpeechRecognizerState.kt | 14 ++ 26 files changed, 624 insertions(+), 204 deletions(-) create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/application/speech/SpeechFacade.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizer.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizerFactory.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/RecognitionListenerAdapter.kt create mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/SpeechRecognizerState.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt new file mode 100644 index 000000000..6ae371031 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt @@ -0,0 +1,20 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech + +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory + +/** An implementation of [SpeechRecognizerFactory] which always fails. */ +object FailingSpeechRecognizerFactory : SpeechRecognizerFactory { + override fun createSpeechRecognizer() = FailingSpeechRecognizer() +} + +/** A [SpeechRecognizer] which will always fail to recognize the user input, and return an error. */ +class FailingSpeechRecognizer : SpeechRecognizer { + private var listener: SpeechRecognizer.Listener? = null + override fun setListener(listener: SpeechRecognizer.Listener) = Unit + override fun startListening() { + listener?.onError() + } + override fun stopListening() = Unit + override fun destroy() = Unit +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/ClassicChessBoardStateTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/ClassicChessBoardStateTest.kt index e878f7901..0cdb1cbe0 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/ClassicChessBoardStateTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/ClassicChessBoardStateTest.kt @@ -10,6 +10,7 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.ui.game.ChessBoardState +import ch.epfl.sdp.mobile.ui.game.SpeechRecognizerState import com.google.common.truth.Truth.assertThat import io.mockk.every import io.mockk.mockk @@ -20,6 +21,11 @@ import org.junit.Test class ClassicChessBoardStateTest { + private object NoOpSpeechRecognizerState : SpeechRecognizerState { + override val listening = false + override fun onListenClick() = Unit + } + @Test fun selectingPiece_displaysAvailableMoves() = runTest { val auth = emptyAuth() @@ -40,7 +46,7 @@ class ClassicChessBoardStateTest { val match = facade.createMatch(user, user) val actions = StatefulGameScreenActions(onBack = {}, onShowAr = {}) - val state = SnapshotChessBoardState(actions, user, match, scope) + val state = SnapshotChessBoardState(actions, user, match, scope, NoOpSpeechRecognizerState) state.onPositionClick(ChessBoardState.Position(4, 6)) assertThat(state.availableMoves) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/CompositionLocalsTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/CompositionLocalsTest.kt index 844fae357..a60508dd2 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/CompositionLocalsTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/CompositionLocalsTest.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.test.junit4.createComposeRule import ch.epfl.sdp.mobile.state.LocalAuthenticationFacade import ch.epfl.sdp.mobile.state.LocalChessFacade import ch.epfl.sdp.mobile.state.LocalSocialFacade +import ch.epfl.sdp.mobile.state.LocalSpeechFacade import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -30,4 +31,11 @@ class CompositionLocalsTest { rule.setContent { LocalSocialFacade.current } } } + + @Test + fun missingSpeechFacade_throwsException() { + assertThrows(IllegalStateException::class.java) { + rule.setContent { LocalSpeechFacade.current } + } + } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/NavigationTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/NavigationTest.kt index e032d51e4..c4d04bff1 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/NavigationTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/NavigationTest.kt @@ -10,6 +10,7 @@ import ch.epfl.sdp.mobile.application.ProfileDocument import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.Navigation import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.SuspendingAuth @@ -18,6 +19,7 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -32,8 +34,9 @@ class NavigationTest { val facade = AuthenticationFacade(SuspendingAuth, store) val socialFacade = SocialFacade(SuspendingAuth, store) val chessFacade = ChessFacade(SuspendingAuth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) rule.setContentWithLocalizedStrings { - ProvideFacades(facade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(facade, socialFacade, chessFacade, speechFacade) { Navigation() } } rule.onAllNodes(keyIsDefined(SemanticsProperties.Text)).assertCountEquals(0) } @@ -45,9 +48,10 @@ class NavigationTest { val facade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(SuspendingAuth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(facade, socialFacade, chessFacade, speechFacade) { Navigation() } } // Do we see the authentication screen actions ? @@ -61,9 +65,10 @@ class NavigationTest { val facade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(SuspendingAuth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(facade, socialFacade, chessFacade, speechFacade) { Navigation() } } facade.signUpWithEmail("email@epfl.ch", "name", "password") @@ -82,9 +87,10 @@ class NavigationTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { Navigation() } } authFacade.signInWithEmail("email@epfl.ch", "password") diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulArScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulArScreenTest.kt index e9a9ca899..5158fca44 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulArScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulArScreenTest.kt @@ -11,12 +11,14 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.HomeActivity import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulArScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import io.mockk.every import io.mockk.mockk import org.junit.Rule @@ -43,13 +45,14 @@ class StatefulArScreenTest { val authApi = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) val user1 = mockk() every { user1.uid } returns "userId1" val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authApi, social, chess) { StatefulArScreen(user1, "gameId") } + ProvideFacades(authApi, social, chess, speech) { StatefulArScreen(user1, "gameId") } } rule.onNodeWithContentDescription(strings.arContentDescription).assertExists() diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulFollowingScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulFollowingScreenTest.kt index 0c854da09..ce36cb983 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulFollowingScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulFollowingScreenTest.kt @@ -10,6 +10,7 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.infrastructure.persistence.store.asFlow import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulFollowingScreen @@ -17,6 +18,7 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import com.google.common.truth.Truth.* import io.mockk.every import io.mockk.mockk @@ -50,13 +52,17 @@ class StatefulFollowingScreenTest { val mockSocialFacade = mockk() val mockAuthenticationFacade = mockk() val mockChessFacade = mockk() + val mockSpeechFacade = SpeechFacade(FailingSpeechRecognizerFactory) every { mockSocialFacade.search("", mockUser) } returns emptyFlow() rule.setContent { - ProvideFacades(mockAuthenticationFacade, mockSocialFacade, mockChessFacade) { - StatefulFollowingScreen(mockUser, {}) - } + ProvideFacades( + mockAuthenticationFacade, + mockSocialFacade, + mockChessFacade, + mockSpeechFacade, + ) { StatefulFollowingScreen(mockUser, {}) } } rule.onNodeWithText("Hans Peter").assertExists() } @@ -72,12 +78,13 @@ class StatefulFollowingScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) authenticationFacade.signUpWithEmail("example@epfl.ch", "name", "password") val user = authenticationFacade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulFollowingScreen(user, {}) } } @@ -105,12 +112,13 @@ class StatefulFollowingScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) authenticationFacade.signUpWithEmail("example@epfl.ch", "name", "password") val user = authenticationFacade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulFollowingScreen(user, {}) } } @@ -138,6 +146,7 @@ class StatefulFollowingScreenTest { authentication = remember { AuthenticationFacade(auth, store) }, social = remember { SocialFacade(auth, store) }, chess = remember { ChessFacade(auth, store) }, + speech = remember { SpeechFacade(FailingSpeechRecognizerFactory) }, ) { StatefulFollowingScreen(user, onShowProfileClick = {}) } } @@ -162,6 +171,7 @@ class StatefulFollowingScreenTest { authentication = remember { AuthenticationFacade(auth, store) }, social = remember { SocialFacade(auth, store) }, chess = remember { ChessFacade(auth, store) }, + speech = remember { SpeechFacade(FailingSpeechRecognizerFactory) }, ) { StatefulFollowingScreen(user, onShowProfileClick = {}) } } @@ -193,6 +203,7 @@ class StatefulFollowingScreenTest { authentication = remember { AuthenticationFacade(auth, store) }, social = remember { SocialFacade(auth, store) }, chess = remember { ChessFacade(auth, store) }, + speech = remember { SpeechFacade(FailingSpeechRecognizerFactory) }, ) { StatefulFollowingScreen(user, onShowProfileClick = {}) } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 726551a3e..e56d11021 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalPermissionsApi::class) + package ch.epfl.sdp.mobile.test.state import androidx.compose.ui.test.* @@ -9,6 +11,7 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.chess.engine.Rank import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.* import ch.epfl.sdp.mobile.test.application.chess.engine.Games.FoolsMate import ch.epfl.sdp.mobile.test.application.chess.engine.Games.Stalemate @@ -17,6 +20,7 @@ import ch.epfl.sdp.mobile.test.application.chess.engine.Games.promote import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import ch.epfl.sdp.mobile.test.ui.game.ChessBoardRobot import ch.epfl.sdp.mobile.test.ui.game.click import ch.epfl.sdp.mobile.test.ui.game.drag @@ -25,6 +29,7 @@ import ch.epfl.sdp.mobile.ui.game.ChessBoardState import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Color.Black import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Color.White import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Rank.* +import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.common.truth.Truth.assertThat import io.mockk.every import io.mockk.mockk @@ -56,13 +61,16 @@ class StatefulGameScreenTest { val authApi = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) val user1 = mockk() every { user1.uid } returns "userId1" val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authApi, social, chess) { StatefulGameScreen(user1, "gameId", actions) } + ProvideFacades(authApi, social, chess, speech) { + StatefulGameScreen(user1, "gameId", actions) + } } return ChessBoardRobot(rule, strings) @@ -529,6 +537,7 @@ class StatefulGameScreenTest { val authApi = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) val user1 = mockk() every { user1.uid } returns "userId1" @@ -537,7 +546,9 @@ class StatefulGameScreenTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authApi, social, chess) { StatefulGameScreen(user1, "gameId", actions) } + ProvideFacades(authApi, social, chess, speech) { + StatefulGameScreen(user1, "gameId", actions) + } } val robot = ChessBoardRobot(rule, strings) @@ -562,6 +573,7 @@ class StatefulGameScreenTest { val authApi = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) val user1 = mockk() every { user1.uid } returns "userId1" @@ -570,7 +582,9 @@ class StatefulGameScreenTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authApi, social, chess) { StatefulGameScreen(user1, "gameId", actions) } + ProvideFacades(authApi, social, chess, speech) { + StatefulGameScreen(user1, "gameId", actions) + } } val robot = ChessBoardRobot(rule, strings) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt index 6e7c5ed0d..90c1251cd 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt @@ -14,6 +14,7 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.Navigation import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulHome @@ -22,6 +23,7 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -41,13 +43,14 @@ class StatefulHomeTest { val api = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) api.signUpWithEmail("email@epfl.ch", "name", "password") val user = api.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(api, social, chess) { StatefulHome(user) } + ProvideFacades(api, social, chess, speech) { StatefulHome(user) } } rule.onNodeWithText(strings.sectionSocial).assertIsSelected() rule.onNodeWithText(strings.sectionSettings).assertIsNotSelected() @@ -60,12 +63,13 @@ class StatefulHomeTest { val api = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) api.signUpWithEmail("email@epfl.ch", "name", "password") val user = api.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(api, social, chess) { StatefulHome(user) } + ProvideFacades(api, social, chess, speech) { StatefulHome(user) } } rule.onNodeWithText(strings.sectionSettings).performClick() rule.onNodeWithText(strings.sectionSocial).assertIsNotSelected() @@ -79,13 +83,14 @@ class StatefulHomeTest { val api = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) api.signUpWithEmail("email@epfl.ch", "name", "password") val user = api.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(api, social, chess) { StatefulHome(user) } + ProvideFacades(api, social, chess, speech) { StatefulHome(user) } } rule.onNodeWithText(strings.sectionSocial).performClick() rule.onNodeWithText(strings.sectionSocial).assertIsSelected() @@ -99,12 +104,13 @@ class StatefulHomeTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email@epfl.ch", "name", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { StatefulHome(user) } + ProvideFacades(facade, social, chess, speech) { StatefulHome(user) } } rule.onNodeWithText(strings.sectionPlay).performClick() rule.onNodeWithText(strings.sectionPlay).assertIsSelected() @@ -123,12 +129,13 @@ class StatefulHomeTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signInWithEmail("email@example.org", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { StatefulHome(user) } + ProvideFacades(facade, social, chess, speech) { StatefulHome(user) } } rule.onNodeWithText(strings.sectionSocial).performClick() rule.onNodeWithText("testName").assertExists() @@ -143,6 +150,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("email@epfl.ch", "name", "password") val user = authFacade.currentUser.filterIsInstance().first() @@ -150,7 +158,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { val controller = rememberNavController() - ProvideFacades(authFacade, socialFacade, chessFacade) { + ProvideFacades(authFacade, socialFacade, chessFacade, speech) { StatefulHome( user = user, // We must call controller.navigate() after the first composition (so the @@ -174,6 +182,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("user1@email", "user1", "password") val currentUser = authFacade.currentUser.filterIsInstance().first() @@ -183,7 +192,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess) { StatefulHome(currentUser) } + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } rule.onNodeWithText(strings.sectionPlay).performClick() @@ -203,6 +212,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("user1@email", "user1", "password") val currentUser = authFacade.currentUser.filterIsInstance().first() @@ -212,7 +222,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess) { StatefulHome(currentUser) } + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } rule.onNodeWithText(strings.sectionPlay).performClick() @@ -233,6 +243,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("user1@email", "user1", "password") val currentUser = authFacade.currentUser.filterIsInstance().first() @@ -242,7 +253,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess) { StatefulHome(currentUser) } + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } rule.onNodeWithText(strings.sectionPlay).performClick() @@ -265,6 +276,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("user1@email", "user1", "password") val currentUser = authFacade.currentUser.filterIsInstance().first() @@ -274,7 +286,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess) { StatefulHome(currentUser) } + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } rule.onNodeWithText(strings.sectionPlay).performClick() @@ -295,13 +307,14 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signUpWithEmail("user1@email", "user1", "password") val currentUser = authFacade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess) { StatefulHome(currentUser) } + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } rule.onNodeWithText(strings.sectionPlay).performClick() @@ -328,6 +341,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signInWithEmail("email@example.org", "password") val user = authFacade.currentUser.filterIsInstance().first() @@ -335,7 +349,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { val controller = rememberNavController() - ProvideFacades(authFacade, socialFacade, chessFacade) { + ProvideFacades(authFacade, socialFacade, chessFacade, speech) { StatefulHome( user = user, controller = controller, @@ -367,6 +381,7 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signInWithEmail("email@example.org", "password") val user = authFacade.currentUser.filterIsInstance().first() @@ -374,7 +389,7 @@ class StatefulHomeTest { val strings = rule.setContentWithLocalizedStrings { val controller = rememberNavController() - ProvideFacades(authFacade, socialFacade, chessFacade) { + ProvideFacades(authFacade, socialFacade, chessFacade, speech) { StatefulHome( user = user, controller = controller, @@ -399,12 +414,13 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signInWithEmail("email@example.org", "password") val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(authFacade, socialFacade, chessFacade, speech) { Navigation() } } rule.onNodeWithText(strings.sectionSettings).performClick() @@ -424,12 +440,13 @@ class StatefulHomeTest { val authFacade = AuthenticationFacade(auth, store) val chessFacade = ChessFacade(auth, store) val socialFacade = SocialFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signInWithEmail("email@example.org", "password") val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { Navigation() } + ProvideFacades(authFacade, socialFacade, chessFacade, speech) { Navigation() } } rule.onNodeWithText(strings.sectionSettings).performClick() diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt index 12fa5c350..9e792359d 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt @@ -9,6 +9,7 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulPlayScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.buildAuth @@ -16,6 +17,7 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import com.google.common.truth.Truth import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.filterIsInstance @@ -44,12 +46,13 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signInWithEmail("email@example.org", "password") val userAuthenticated = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = userAuthenticated, onGameItemClick = {}, @@ -65,85 +68,87 @@ class StatefulPlayScreenTest { @Test fun given_playerHasLostByCheckmate_when_accessingPlayScreen_then_displayLostByCheckmate() = runTest { - val auth = buildAuth { user("email@example.org", "password", "1") } - val store = buildStore { - collection("users") { - document("1", ProfileDocument("1")) - document("2", ProfileDocument("2", name = "test")) - } - collection("games") { - document( - /* Funfact: Fool's Mate, Fastest checkmate possible https://www.chess.com/article/view/fastest-chess-checkmates */ - "id", - ChessDocument( - uid = "786", - whiteId = "1", - blackId = "2", - moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) - } - } - - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - - facade.signInWithEmail("email@example.org", "password") - val userAuthenticated = facade.currentUser.filterIsInstance().first() - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { - StatefulPlayScreen( - user = userAuthenticated, - onGameItemClick = {}, - navigateToPrepareGame = {}, - navigateToLocalGame = {}, - ) + val auth = buildAuth { user("email@example.org", "password", "1") } + val store = buildStore { + collection("users") { + document("1", ProfileDocument("1")) + document("2", ProfileDocument("2", name = "test")) + } + collection("games") { + document( + /* Funfact: Fool's Mate, Fastest checkmate possible https://www.chess.com/article/view/fastest-chess-checkmates */ + "id", + ChessDocument( + uid = "786", + whiteId = "1", + blackId = "2", + moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) } } - rule.onNodeWithText(strings.profileLostByCheckmate(4)).assertExists() - } + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signInWithEmail("email@example.org", "password") + val userAuthenticated = facade.currentUser.filterIsInstance().first() + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = userAuthenticated, + onGameItemClick = {}, + navigateToPrepareGame = {}, + navigateToLocalGame = {}, + ) + } + } + + rule.onNodeWithText(strings.profileLostByCheckmate(4)).assertExists() + } @Test fun given_playerHasWonByCheckmate_when_accessingPlayScreen_then_displayWinByCheckmate() = runTest { - val auth = buildAuth { user("email@example.org", "password", "1") } - val store = buildStore { - collection("users") { - document("1", ProfileDocument("1")) - document("2", ProfileDocument("2", name = "test")) - } - collection("games") { - document( - "id", - ChessDocument( - uid = "786", - whiteId = "2", - blackId = "1", - moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) - } - } - - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - - facade.signInWithEmail("email@example.org", "password") - val userAuthenticated = facade.currentUser.filterIsInstance().first() - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { - StatefulPlayScreen( - user = userAuthenticated, - onGameItemClick = {}, - navigateToPrepareGame = {}, - navigateToLocalGame = {}, - ) + val auth = buildAuth { user("email@example.org", "password", "1") } + val store = buildStore { + collection("users") { + document("1", ProfileDocument("1")) + document("2", ProfileDocument("2", name = "test")) + } + collection("games") { + document( + "id", + ChessDocument( + uid = "786", + whiteId = "2", + blackId = "1", + moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) } } - rule.onNodeWithText(strings.profileWonByCheckmate(4)).assertExists() - } + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signInWithEmail("email@example.org", "password") + val userAuthenticated = facade.currentUser.filterIsInstance().first() + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = userAuthenticated, + onGameItemClick = {}, + navigateToPrepareGame = {}, + navigateToLocalGame = {}, + ) + } + } + + rule.onNodeWithText(strings.profileWonByCheckmate(4)).assertExists() + } @Test fun statefulPlayScreen_isDisplayedWithNoWhiteId() = runTest { @@ -158,12 +163,13 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signInWithEmail("email@example.org", "password") val userAuthenticated = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = userAuthenticated, onGameItemClick = {}, @@ -189,12 +195,13 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signInWithEmail("email@example.org", "password") val userAuthenticated = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = userAuthenticated, onGameItemClick = {}, @@ -214,12 +221,13 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email@example.org", "test", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = user, onGameItemClick = {}, @@ -239,13 +247,14 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = currentUser, onGameItemClick = {}, @@ -269,6 +278,7 @@ class StatefulPlayScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -276,7 +286,7 @@ class StatefulPlayScreenTest { val channel = Channel(capacity = 1) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPlayScreen( user = currentUser, onGameItemClick = {}, @@ -298,36 +308,37 @@ class StatefulPlayScreenTest { @Test fun given_playScreen_when_clickingNewGameAndOnlinePlay_then_onlineGameCallbackIsCalled() = runTest { - val auth = emptyAuth() - val store = buildStore { - collection("users") { document("userId2", ProfileDocument(name = "user2")) } - } - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - - facade.signUpWithEmail("user1@email", "user1", "password") - val currentUser = facade.currentUser.filterIsInstance().first() - - val channel = Channel(capacity = 1) - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { - StatefulPlayScreen( - user = currentUser, - onGameItemClick = {}, - navigateToPrepareGame = { - channel.trySend(Unit) - channel.close() - }, - navigateToLocalGame = {}, - ) - } + val auth = emptyAuth() + val store = buildStore { + collection("users") { document("userId2", ProfileDocument(name = "user2")) } } - - rule.onNodeWithText(strings.newGame).performClick() - rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() - - Truth.assertThat(channel.tryReceive().getOrNull()).isEqualTo(Unit) - } + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signUpWithEmail("user1@email", "user1", "password") + val currentUser = facade.currentUser.filterIsInstance().first() + + val channel = Channel(capacity = 1) + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = currentUser, + onGameItemClick = {}, + navigateToPrepareGame = { + channel.trySend(Unit) + channel.close() + }, + navigateToLocalGame = {}, + ) + } + } + + rule.onNodeWithText(strings.newGame).performClick() + rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() + + Truth.assertThat(channel.tryReceive().getOrNull()).isEqualTo(Unit) + } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPrepareGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPrepareGameScreenTest.kt index 4f93bc4c6..5f76ebe39 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPrepareGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPrepareGameScreenTest.kt @@ -9,12 +9,14 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulPrepareGameScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.filterIsInstance @@ -35,12 +37,13 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email@epfl.ch", "name", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = user, navigateToGame = {}, cancelClick = {}) } } @@ -56,12 +59,13 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email@epfl.ch", "name", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = user, navigateToGame = {}, cancelClick = {}) } } @@ -77,12 +81,13 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email@epfl.ch", "name", "password") val user = facade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = user, navigateToGame = {}, cancelClick = {}) } } @@ -101,6 +106,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val authUser1 = facade.currentUser.filterIsInstance().first() @@ -109,7 +115,7 @@ class StatefulPrepareGameScreenTest { authUser1.follow(user2) rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = authUser1, navigateToGame = {}, cancelClick = {}) } } @@ -126,6 +132,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -135,7 +142,7 @@ class StatefulPrepareGameScreenTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = currentUser, navigateToGame = {}, cancelClick = {}) } } @@ -157,6 +164,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -166,7 +174,7 @@ class StatefulPrepareGameScreenTest { val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = currentUser, navigateToGame = {}, cancelClick = {}) } } @@ -189,6 +197,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val authUser1 = facade.currentUser.filterIsInstance().first() @@ -197,7 +206,7 @@ class StatefulPrepareGameScreenTest { authUser1.follow(user2) rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen(user = authUser1, navigateToGame = {}, cancelClick = {}) } } @@ -216,6 +225,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -226,7 +236,7 @@ class StatefulPrepareGameScreenTest { val channel = Channel(capacity = 1) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen( user = currentUser, navigateToGame = { _ -> @@ -252,6 +262,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -262,7 +273,7 @@ class StatefulPrepareGameScreenTest { val channel = Channel(capacity = 1) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen( user = currentUser, navigateToGame = {}, @@ -287,6 +298,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -297,7 +309,7 @@ class StatefulPrepareGameScreenTest { val channel = Channel(capacity = 1) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen( user = currentUser, navigateToGame = { @@ -321,6 +333,7 @@ class StatefulPrepareGameScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("user1@email", "user1", "password") val currentUser = facade.currentUser.filterIsInstance().first() @@ -331,7 +344,7 @@ class StatefulPrepareGameScreenTest { val channel = Channel(capacity = 1) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess) { + ProvideFacades(facade, social, chess, speech) { StatefulPrepareGameScreen( user = currentUser, navigateToGame = { diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulProfileScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulProfileScreenTest.kt index f58bc4d65..7a6d96bec 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulProfileScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulProfileScreenTest.kt @@ -7,11 +7,13 @@ import ch.epfl.sdp.mobile.application.ProfileDocument import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulVisitedProfileScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.buildAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -37,10 +39,11 @@ class StatefulProfileScreenTest { val authFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { + ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { StatefulVisitedProfileScreen("1") } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingScreenTest.kt index 9adfc325f..c1af81166 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingScreenTest.kt @@ -8,10 +8,12 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulSettingsScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import com.google.common.truth.Truth.assertThat import io.mockk.every import io.mockk.mockk @@ -25,33 +27,34 @@ class StatefulSettingScreenTest { @Test fun given_SettingScreenLoaded_when_clickingOnEditProfileName_then_functionShouldBeCalled() = runTest { - val user = mockk() - every { user.name } returns "test" - every { user.email } returns "test" - every { user.emoji } returns "test" - every { user.backgroundColor } returns Profile.Color.Orange - every { user.uid } returns "test" - every { user.followed } returns false - var functionCalled = false - - val openProfileEditNameMock = { functionCalled = true } - - val auth = emptyAuth() - val store = emptyStore() - - val authFacade = AuthenticationFacade(auth, store) - val socialFacade = SocialFacade(auth, store) - val chessFacade = ChessFacade(auth, store) - - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { - StatefulSettingsScreen(user, openProfileEditNameMock) - } - } - - rule.onNodeWithContentDescription(strings.profileEditNameIcon).performClick() - - assertThat(functionCalled).isTrue() - } + val user = mockk() + every { user.name } returns "test" + every { user.email } returns "test" + every { user.emoji } returns "test" + every { user.backgroundColor } returns Profile.Color.Orange + every { user.uid } returns "test" + every { user.followed } returns false + var functionCalled = false + + val openProfileEditNameMock = { functionCalled = true } + + val auth = emptyAuth() + val store = emptyStore() + + val authFacade = AuthenticationFacade(auth, store) + val socialFacade = SocialFacade(auth, store) + val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) + + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { + StatefulSettingsScreen(user, openProfileEditNameMock) + } + } + + rule.onNodeWithContentDescription(strings.profileEditNameIcon).performClick() + + assertThat(functionCalled).isTrue() + } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt index 5907cf723..a078cf3bc 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt @@ -8,11 +8,13 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulSettingsScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.buildAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -40,13 +42,14 @@ class StatefulSettingsScreenTest { val authFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) authFacade.signInWithEmail("email@example.org", "password") val user = authFacade.currentUser.filterIsInstance().first() val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade) { + ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { StatefulSettingsScreen(user, {}) } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt index 21b9d0d42..f80225567 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt @@ -11,10 +11,12 @@ import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulSpeechRecognitionScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import ch.epfl.sdp.mobile.ui.speech_recognition.DefaultText import ch.epfl.sdp.mobile.ui.speech_recognition.ListeningText import ch.epfl.sdp.mobile.ui.speech_recognition.MicroIconDescription @@ -40,12 +42,13 @@ class StatefulSpeechRecognitionScreenTest { val facade = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) facade.signUpWithEmail("email", "name", "password") val user = facade.currentUser.filterIsInstance().first() rule.setContent { - ProvideFacades(facade, social, chess) { StatefulSpeechRecognitionScreen(user) } + ProvideFacades(facade, social, chess, speech) { StatefulSpeechRecognitionScreen(user) } } rule.onNodeWithText(PermissionGranted).assertExists() diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/authentication/AuthenticationScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/authentication/AuthenticationScreenTest.kt index 7365c0880..01557ec92 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/authentication/AuthenticationScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/authentication/AuthenticationScreenTest.kt @@ -8,11 +8,13 @@ import androidx.compose.ui.test.junit4.createComposeRule import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.state.ProvideFacades import ch.epfl.sdp.mobile.state.StatefulAuthenticationScreen import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.buildAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory import ch.epfl.sdp.mobile.test.state.setContentWithLocalizedStrings import ch.epfl.sdp.mobile.ui.authentication.AuthenticationScreen import ch.epfl.sdp.mobile.ui.authentication.AuthenticationScreenState @@ -63,9 +65,10 @@ class AuthenticationScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulAuthenticationScreen() } } @@ -84,9 +87,10 @@ class AuthenticationScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulAuthenticationScreen() } } @@ -105,9 +109,10 @@ class AuthenticationScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulAuthenticationScreen() } } @@ -126,9 +131,10 @@ class AuthenticationScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulAuthenticationScreen() } } @@ -147,9 +153,10 @@ class AuthenticationScreenTest { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) val strings = rule.setContentWithLocalizedStrings { - ProvideFacades(authenticationFacade, socialFacade, chessFacade) { + ProvideFacades(authenticationFacade, socialFacade, chessFacade, speechFacade) { StatefulAuthenticationScreen() } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/application/speech/SpeechFacade.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/application/speech/SpeechFacade.kt new file mode 100644 index 000000000..2d2ede3bc --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/application/speech/SpeechFacade.kt @@ -0,0 +1,64 @@ +package ch.epfl.sdp.mobile.application.speech + +import ch.epfl.sdp.mobile.application.speech.SpeechFacade.RecognitionResult.* +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * A facade which provides access to functions to perform some voice recognition. + * + * @param factory the [SpeechRecognizerFactory] which is used internally by this [SpeechFacade]. + */ +class SpeechFacade(private val factory: SpeechRecognizerFactory) { + + /** The result of a call to [SpeechFacade.recognize]. */ + sealed interface RecognitionResult { + + /** Indicates that a failure occurred. */ + sealed interface Failure : RecognitionResult { + + /** Indicates that a failure occurred and no speech was recognized. */ + object Internal : Failure + } + + /** + * Indicates a success of recognition. The available [results] are sorted by decreasing score + * (the most relevant results come first). + * + * @param results the [List] of possible results. + */ + data class Success(val results: List) : RecognitionResult + } + + /** + * Starts voice recognition, and returns the associated [RecognitionResult]. + * + * @return the [RecognitionResult] from the recognition request. + */ + suspend fun recognize(): RecognitionResult = suspendCancellableCoroutine { cont -> + val recognizer = factory.createSpeechRecognizer() + + /** Cleans up the recognizer. */ + fun cleanup() { + recognizer.stopListening() + recognizer.destroy() + } + + recognizer.setListener( + object : SpeechRecognizer.Listener { + override fun onError() { + cleanup() + cont.resume(Failure.Internal) + } + override fun onResults(results: List) { + cleanup() + cont.resume(Success(results)) + } + }, + ) + recognizer.startListening() + cont.invokeOnCancellation { cleanup() } + } +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizer.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizer.kt new file mode 100644 index 000000000..92faa5e4b --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizer.kt @@ -0,0 +1,35 @@ +package ch.epfl.sdp.mobile.infrastructure.speech + +/** An interface providing access to the native [SpeechRecognizer] of the platform. */ +interface SpeechRecognizer { + + /** A listener which will be called when some new speech recognition results are available. */ + interface Listener { + + /** A callback method, called when there's an error during the recognition. */ + fun onError() + + /** + * A callback method, called with the list of results. + * + * @param results the [List] of speech recognition results, ordered by decreasing score. + */ + fun onResults(results: List) + } + + /** + * Sets the [Listener] for this [SpeechRecognizer]. + * + * @param listener the [Listener] which is set. + */ + fun setListener(listener: Listener) + + /** Starts listening with this [SpeechRecognizer]. */ + fun startListening() + + /** Stops listening with this [SpeechRecognizer]. */ + fun stopListening() + + /** Destroys the recognizer. */ + fun destroy() +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizerFactory.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizerFactory.kt new file mode 100644 index 000000000..aec182453 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/SpeechRecognizerFactory.kt @@ -0,0 +1,8 @@ +package ch.epfl.sdp.mobile.infrastructure.speech + +/** A factory which can create some [SpeechRecognizer] instances. */ +interface SpeechRecognizerFactory { + + /** Returns a new [SpeechRecognizer], which may be used to perform some voice recognition. */ + fun createSpeechRecognizer(): SpeechRecognizer +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt new file mode 100644 index 000000000..2cf1c5f5f --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt @@ -0,0 +1,72 @@ +package ch.epfl.sdp.mobile.infrastructure.speech.android + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.speech.RecognizerIntent.* +import android.speech.SpeechRecognizer as NativeSpeechRecognizer +import android.speech.SpeechRecognizer.RESULTS_RECOGNITION +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory + +/** The default locale we'll be using for speech recognition. */ +private const val DefaultLanguage = "en-US" + +/** The default maximum number of results we'll receive from the system speech recognizer. */ +private const val DefaultResultsCount = 10 + +/** + * An implementation of a [SpeechRecognizer] which is backed by a [NativeSpeechRecognizer]. + * + * @param context the [Context] which will be used to create the speech recognizers. + * @param language the default language to use. + * @param resultsCount the maximum results count for each result set. + */ +class AndroidSpeechRecognizerFactory( + private val context: Context, + private val language: String = DefaultLanguage, + private val resultsCount: Int = DefaultResultsCount, +) : SpeechRecognizerFactory { + + override fun createSpeechRecognizer(): SpeechRecognizer = + AndroidSpeechRecognizer( + recognizer = NativeSpeechRecognizer.createSpeechRecognizer(context), + language = language, + resultsCount = resultsCount, + ) +} + +/** + * An implementation of a [SpeechRecognizer] which is backed by a [NativeSpeechRecognizer]. + * + * @param recognizer the underlying [NativeSpeechRecognizer]. + * @param language the language code that we use for recognition. + * @param resultsCount the maximum count of results that we are interested in. + */ +class AndroidSpeechRecognizer( + private val recognizer: NativeSpeechRecognizer, + private val language: String, + private val resultsCount: Int, +) : SpeechRecognizer { + + override fun setListener(listener: SpeechRecognizer.Listener) = + recognizer.setRecognitionListener( + object : RecognitionListenerAdapter() { + override fun onError(error: Int) = listener.onError() + override fun onResults( + results: Bundle?, + ) = listener.onResults(results?.getStringArrayList(RESULTS_RECOGNITION) ?: emptyList()) + }, + ) + + override fun startListening() = + recognizer.startListening( + Intent(ACTION_RECOGNIZE_SPEECH) + .putExtra(EXTRA_LANGUAGE, language) + .putExtra(EXTRA_MAX_RESULTS, resultsCount), + ) + + override fun stopListening() = recognizer.stopListening() + + override fun destroy() = recognizer.destroy() +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/RecognitionListenerAdapter.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/RecognitionListenerAdapter.kt new file mode 100644 index 000000000..551aa8784 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/RecognitionListenerAdapter.kt @@ -0,0 +1,20 @@ +package ch.epfl.sdp.mobile.infrastructure.speech.android + +import android.os.Bundle +import android.speech.RecognitionListener + +/** + * An abstract adapter around the Android [RecognitionListener] interface which provides some + * default implementations for all the methods. + */ +abstract class RecognitionListenerAdapter : RecognitionListener { + override fun onReadyForSpeech(params: Bundle?) = Unit + override fun onBeginningOfSpeech() = Unit + override fun onRmsChanged(rmsdB: Float) = Unit + override fun onBufferReceived(buffer: ByteArray?) = Unit + override fun onEndOfSpeech() = Unit + override fun onError(error: Int) = Unit + override fun onResults(results: Bundle?) = Unit + override fun onPartialResults(partialResults: Bundle?) = Unit + override fun onEvent(eventType: Int, params: Bundle?) = Unit +} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt index dcdec0b3d..db3eee846 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.compositionLocalOf import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade /** A global composition local which provides access to an instance of [AuthenticationFacade]. */ val LocalAuthenticationFacade = @@ -17,6 +18,9 @@ val LocalSocialFacade = compositionLocalOf { error("Missing Social /** A global composition local which provides access to an instance of [ChessFacade]. */ val LocalChessFacade = compositionLocalOf { error("Missing Chess API.") } +/** A global composition local which provides access to an instance of [SpeechFacade]. */ +val LocalSpeechFacade = compositionLocalOf { error("Missing Speech Facade.") } + /** * Provides the given Faces through different [androidx.compose.runtime.CompositionLocal] values * available throughout the hierarchy. @@ -29,11 +33,13 @@ fun ProvideFacades( authentication: AuthenticationFacade, social: SocialFacade, chess: ChessFacade, + speech: SpeechFacade, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalAuthenticationFacade provides authentication, LocalSocialFacade provides social, LocalChessFacade provides chess, + LocalSpeechFacade provides speech, ) { content() } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/HomeActivity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/HomeActivity.kt index 15602336d..06987b308 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/HomeActivity.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/HomeActivity.kt @@ -6,8 +6,10 @@ import androidx.activity.compose.setContent import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.social.SocialFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade import ch.epfl.sdp.mobile.infrastructure.persistence.auth.firebase.FirebaseAuth import ch.epfl.sdp.mobile.infrastructure.persistence.store.firestore.FirestoreStore +import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizerFactory import ch.epfl.sdp.mobile.ui.PawniesTheme import com.google.firebase.auth.ktx.auth import com.google.firebase.firestore.ktx.firestore @@ -26,14 +28,17 @@ class HomeActivity : ComponentActivity() { val authenticationFacade = AuthenticationFacade(auth, store) val socialFacade = SocialFacade(auth, store) val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(AndroidSpeechRecognizerFactory(this)) setContent { PawniesTheme { ProvideLocalizedStrings { ProvideFacades( - authentication = authenticationFacade, social = socialFacade, chess = chessFacade) { - Navigation() - } + authentication = authenticationFacade, + social = socialFacade, + chess = chessFacade, + speech = speechFacade, + ) { Navigation() } } } } diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt index 31da2e999..0b8ecc0cd 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt @@ -1,6 +1,12 @@ +@file:OptIn(ExperimentalPermissionsApi::class) + package ch.epfl.sdp.mobile.state +import android.Manifest.permission.RECORD_AUDIO +import androidx.compose.foundation.MutatePriority.UserInput +import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Modifier import ch.epfl.sdp.mobile.application.Profile @@ -11,11 +17,16 @@ import ch.epfl.sdp.mobile.application.chess.engine.Color.Black import ch.epfl.sdp.mobile.application.chess.engine.Color.White import ch.epfl.sdp.mobile.application.chess.engine.rules.Action import ch.epfl.sdp.mobile.application.chess.notation.AlgebraicNotation.toAlgebraicNotation +import ch.epfl.sdp.mobile.application.speech.SpeechFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade.RecognitionResult.* import ch.epfl.sdp.mobile.state.SnapshotChessBoardState.SnapshotPiece import ch.epfl.sdp.mobile.ui.game.* import ch.epfl.sdp.mobile.ui.game.GameScreenState.Message import ch.epfl.sdp.mobile.ui.game.GameScreenState.Message.* import ch.epfl.sdp.mobile.ui.game.GameScreenState.Move +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -38,6 +49,7 @@ data class StatefulGameScreenActions( * @param actions the [StatefulGameScreenActions] to perform. * @param modifier the [Modifier] for the composable. * @param paddingValues the [PaddingValues] for this composable. + * @param audioPermissionState the [PermissionState] which provides access to audio content. */ @Composable fun StatefulGameScreen( @@ -46,11 +58,24 @@ fun StatefulGameScreen( actions: StatefulGameScreenActions, modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(), + audioPermissionState: PermissionState = rememberPermissionState(RECORD_AUDIO), ) { - val facade = LocalChessFacade.current - val scope = rememberCoroutineScope() - val match = remember(facade, id) { facade.match(id) } + val chessFacade = LocalChessFacade.current + val speechFacade = LocalSpeechFacade.current + val scope = rememberCoroutineScope() + val match = remember(chessFacade, id) { chessFacade.match(id) } + + val snackbarHostState = remember { SnackbarHostState() } + val speechRecognizerState = + remember(audioPermissionState, speechFacade, snackbarHostState, scope) { + SnackbarSpeechRecognizerState( + permission = audioPermissionState, + facade = speechFacade, + snackbarHostState = snackbarHostState, + scope = scope, + ) + } val gameScreenState = remember(actions, user, match, scope) { SnapshotChessBoardState( @@ -58,6 +83,7 @@ fun StatefulGameScreen( user = user, match = match, scope = scope, + speechRecognizerState = speechRecognizerState, ) } @@ -67,6 +93,7 @@ fun StatefulGameScreen( state = gameScreenState, modifier = modifier, contentPadding = paddingValues, + snackbarHostState = snackbarHostState, ) } @@ -95,6 +122,54 @@ private fun StatefulPromoteDialog( } } +/** + * An implementation of [SpeechRecognizerState] which will display the results in a + * [SnackbarHostState]. + * + * @param permission the [PermissionState] for the microphone permission. + * @param facade the [SpeechFacade] which is used. + * @param snackbarHostState the [SnackbarHostState] used to display some results. + * @param scope the [CoroutineScope] in which the actions are performed. + */ +class SnackbarSpeechRecognizerState +constructor( + private val permission: PermissionState, + private val facade: SpeechFacade, + private val snackbarHostState: SnackbarHostState, + private val scope: CoroutineScope, +) : SpeechRecognizerState { + + override var listening: Boolean by mutableStateOf(false) + private set + + /** + * A [MutatorMutex] which ensures that multiple speech recognition requests aren't performed + * simultaneously, and that clicking on the button again cancels the previous request. + */ + private val mutex = MutatorMutex() + + override fun onListenClick() { + scope.launch { + mutex.mutate(UserInput) { + try { + listening = true + if (!permission.hasPermission) { + permission.launchPermissionRequest() + } else { + when (val speech = facade.recognize()) { + // TODO : Display an appropriate message, otherwise act on the board. + Failure.Internal -> snackbarHostState.showSnackbar("Internal failure") + is Success -> for (result in speech.results) snackbarHostState.showSnackbar(result) + } + } + } finally { + listening = false + } + } + } + } +} + /** * An implementation of [GameScreenState] and [PromotionState] that starts with default chess * positions, can move pieces and has a static move list. @@ -109,14 +184,8 @@ class SnapshotChessBoardState( private val user: AuthenticatedUser, private val match: Match, private val scope: CoroutineScope, -) : GameScreenState, PromotionState { - - // TODO : Implement these things. - override var listening by mutableStateOf(false) - private set - override fun onListenClick() { - listening = !listening - } + speechRecognizerState: SpeechRecognizerState, +) : GameScreenState, PromotionState, SpeechRecognizerState by speechRecognizerState { override fun onArClick() = actions.onShowAr(match) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt index 802dda97a..71a1d9bad 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.key +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -37,9 +38,12 @@ fun GameScreen( state: GameScreenState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ) { + // FIXME : This composable might require some refined handling of snackbar content padding. Scaffold( modifier = modifier, + scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState), topBar = { GameScreenTopBar( onBackClick = state::onBackClick, diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreenState.kt index 8e08b5046..b7fa2aaa1 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreenState.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreenState.kt @@ -9,7 +9,8 @@ import androidx.compose.runtime.Stable * @param Piece the type of the pieces of the underlying [ChessBoardState]. */ @Stable -interface GameScreenState : MovableChessBoardState { +interface GameScreenState : + MovableChessBoardState, SpeechRecognizerState { /** * A class representing a [Move] that's been performed by one of the players. @@ -50,12 +51,6 @@ interface GameScreenState : MovableChessBoardStat /** A callback which will be invoked when the user clicks on the AR button. */ fun onArClick() - /** A [Boolean] which indicates if the device is currently listening to voice inputs. */ - val listening: Boolean - - /** A callback which will be invoked when the user clicks on the listening button. */ - fun onListenClick() - /** * A [List] of all the moves which have been performed by the user. Moves are ordered and should * be displayed as such. diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/SpeechRecognizerState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/SpeechRecognizerState.kt new file mode 100644 index 000000000..634b14d88 --- /dev/null +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/SpeechRecognizerState.kt @@ -0,0 +1,14 @@ +package ch.epfl.sdp.mobile.ui.game + +import androidx.compose.runtime.Stable + +/** An interface which represents the state of the user */ +@Stable +interface SpeechRecognizerState { + + /** A [Boolean] which indicates if the device is currently listening to voice inputs. */ + val listening: Boolean + + /** A callback which will be invoked when the user clicks on the listening button. */ + fun onListenClick() +} From dabe0f5d506838f883a0ffcc0af7b95b2b1a013f Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 18:41:54 +0200 Subject: [PATCH 28/42] Remove some legacy code --- .../StatefulSpeechRecognitionScreenTest.kt | 60 -------- .../SpeechRecognitionScreenTest.kt | 128 ------------------ .../ch/epfl/sdp/mobile/state/StatefulHome.kt | 8 -- .../state/StatefulSpeechRecognitionScreen.kt | 49 ------- .../epfl/sdp/mobile/ui/home/HomeScaffold.kt | 2 - .../ch/epfl/sdp/mobile/ui/i18n/English.kt | 1 - .../sdp/mobile/ui/i18n/LocalizedStrings.kt | 1 - .../SpeechRecognitionScreen.kt | 126 ----------------- .../SpeechRecognitionScreenState.kt | 20 --- .../speech_recognition/SpeechRecognizable.kt | 13 -- .../SpeechRecognizerEntity.kt | 65 --------- 11 files changed, 473 deletions(-) delete mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt delete mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt delete mode 100644 mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt deleted file mode 100644 index f80225567..000000000 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSpeechRecognitionScreenTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ch.epfl.sdp.mobile.test.state - -import android.Manifest -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.rule.GrantPermissionRule -import androidx.test.rule.GrantPermissionRule.grant -import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser -import ch.epfl.sdp.mobile.application.authentication.AuthenticationFacade -import ch.epfl.sdp.mobile.application.chess.ChessFacade -import ch.epfl.sdp.mobile.application.social.SocialFacade -import ch.epfl.sdp.mobile.application.speech.SpeechFacade -import ch.epfl.sdp.mobile.state.ProvideFacades -import ch.epfl.sdp.mobile.state.StatefulSpeechRecognitionScreen -import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth -import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.emptyStore -import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory -import ch.epfl.sdp.mobile.ui.speech_recognition.DefaultText -import ch.epfl.sdp.mobile.ui.speech_recognition.ListeningText -import ch.epfl.sdp.mobile.ui.speech_recognition.MicroIconDescription -import ch.epfl.sdp.mobile.ui.speech_recognition.PermissionGranted -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -@ExperimentalPermissionsApi -class StatefulSpeechRecognitionScreenTest { - - @get:Rule val rule = createComposeRule() - - @get:Rule val permissionRule: GrantPermissionRule = grant(Manifest.permission.RECORD_AUDIO) - - @Test - fun given_defaultScreen_when_micActivatedAndDeactivated_then_defaultState() = runTest { - val auth = emptyAuth() - val store = emptyStore() - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) - - facade.signUpWithEmail("email", "name", "password") - val user = facade.currentUser.filterIsInstance().first() - - rule.setContent { - ProvideFacades(facade, social, chess, speech) { StatefulSpeechRecognitionScreen(user) } - } - - rule.onNodeWithText(PermissionGranted).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(ListeningText).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(DefaultText).assertExists() - } -} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt deleted file mode 100644 index 163cc89bc..000000000 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/ui/speech_recognition/SpeechRecognitionScreenTest.kt +++ /dev/null @@ -1,128 +0,0 @@ -package ch.epfl.sdp.mobile.test.ui.speech_recognition - -import android.Manifest -import android.os.Bundle -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.rule.GrantPermissionRule -import ch.epfl.sdp.mobile.state.DefaultSpeechRecognitionScreenState -import ch.epfl.sdp.mobile.ui.speech_recognition.* -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.test.runTest -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.core.IsEqual -import org.junit.Rule -import org.junit.Test - -@ExperimentalPermissionsApi -class SpeechRecognitionScreenTest { - - @get:Rule val rule = createComposeRule() - - @Test - fun given_noGrantedPermission_when_screenDisplayed_then_noPermissionText() { - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns false - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) - rule.setContent { SpeechRecognitionScreen(state) } - - rule.onNodeWithText(PermissionDenied).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists() - rule.onNodeWithText(DefaultText).assertExists() - } - - @get:Rule - val permissionRule: GrantPermissionRule = - GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) - - @Test - fun given_defaultScreenState_when_showed_then_displayedDefaultScreen() { - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns false - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(false)) - rule.setContent { SpeechRecognitionScreen(state) } - // Permission is granted by default :( - rule.onNodeWithText(PermissionDenied).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists() - rule.onNodeWithText(DefaultText).assertExists() - } - - @Test - fun given_defaultScreenState_when_micClicked_then_ListeningDisplayed() { - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns true - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) - - rule.setContent { SpeechRecognitionScreen(state) } - rule.onNodeWithText(PermissionGranted).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(ListeningText).assertExists() - } - - @Test - fun given_listeningMic_when_micClicked_then_stopListening() { - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns true - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) - - rule.setContent { SpeechRecognitionScreen(state) } - rule.onNodeWithText(PermissionGranted).assertExists() - rule.onNodeWithText(DefaultText).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(ListeningText).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - - rule.onNodeWithText(ListeningText).assertDoesNotExist() - rule.onNodeWithText(DefaultText).assertExists() - } - - @Test - fun given_listeningMic_when_talking_then_textDisplayed() { - val mockedPermission = mockk() - every { mockedPermission.hasPermission } returns true - - val state = DefaultSpeechRecognitionScreenState(mockedPermission, mutableStateOf(true)) - - val speech = "Hello World" - - val mockedRecognizer: SpeechRecognizable = mockk() - coEvery { mockedRecognizer.recognition(any()) } returns listOf(speech) - - rule.setContent { SpeechRecognitionScreen(state, recognizer = mockedRecognizer) } - rule.onNodeWithText(PermissionGranted).assertExists() - rule.onNodeWithText(DefaultText).assertExists() - rule.onNodeWithContentDescription(MicroIconDescription).assertExists().performClick() - rule.onNodeWithText(speech).assertExists() - } - - @Test - fun testListener() = runTest { - val testResults = arrayListOf("Hello", "World") - val listenerResults = - suspendCancellableCoroutine> { cont -> - val bundle: Bundle = mockk() - every { bundle.getStringArrayList(any()) } returns testResults - defaultListener(cont).onResults(bundle) - } - assertThat(listenerResults, IsEqual(testResults)) - } - - @Test - fun testListenerEmpty() = runTest { - val listenerResults = - suspendCancellableCoroutine> { cont -> - val bundle: Bundle = mockk() - every { bundle.getStringArrayList(any()) } returns null - defaultListener(cont).onResults(bundle) - } - assertThat(listenerResults, IsEqual(emptyList())) - } -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt index 22b011f84..5e8b62918 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt @@ -39,9 +39,6 @@ private const val GameDefaultId = "" /** The route associated to new game button in play screen */ private const val PrepareGameRoute = "prepare_game" -/** Temporary route associated with speech recognition demo screen */ -private const val SpeechRecognitionRoute = "speech" - /** The route associated to the ar tab. */ private const val ArRoute = "ar" @@ -138,9 +135,6 @@ fun StatefulHome( paddingValues = paddingValues, ) } - composable(SpeechRecognitionRoute) { - StatefulSpeechRecognitionScreen(user, Modifier.fillMaxSize()) - } composable("$ArRoute/{id}") { entry -> val id = requireNotNull(entry.arguments).getString("id", GameDefaultId) StatefulArScreen(id, Modifier.fillMaxSize()) @@ -154,7 +148,6 @@ private fun NavBackStackEntry.toSection(): HomeSection = when (destination.route) { SettingsRoute -> HomeSection.Settings PlayRoute -> HomeSection.Play - SpeechRecognitionRoute -> HomeSection.SpeechRecognition else -> HomeSection.Social } @@ -164,7 +157,6 @@ private fun HomeSection.toRoute(): String = HomeSection.Social -> SocialRoute HomeSection.Settings -> SettingsRoute HomeSection.Play -> PlayRoute - HomeSection.SpeechRecognition -> SpeechRecognitionRoute } private fun hideBar(route: String?): Boolean { diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt deleted file mode 100644 index 24ae8f397..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulSpeechRecognitionScreen.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ch.epfl.sdp.mobile.state - -import android.Manifest -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser -import ch.epfl.sdp.mobile.ui.speech_recognition.SpeechRecognitionScreen -import ch.epfl.sdp.mobile.ui.speech_recognition.SpeechRecognitionScreenState -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.rememberPermissionState - -/** - * Default implementation of the [SpeechRecognitionScreenState] - * @property permissionState [PermissionState] for launching permission dialog - * @property microphonePermissionState mutable state of microphone permission - */ -@ExperimentalPermissionsApi -class DefaultSpeechRecognitionScreenState( - override val permissionState: PermissionState, - private val microphonePermissionState: MutableState -) : SpeechRecognitionScreenState { - - override var hasMicrophonePermission by microphonePermissionState - override fun onPermissionChange() { - hasMicrophonePermission = permissionState.hasPermission - } -} - -/** - * Stateful composable for the SpeechRecognitionScreen - * @param user authenticated user - * @param modifier [Modifier] of this composable - */ -@ExperimentalPermissionsApi -@Composable -fun StatefulSpeechRecognitionScreen(user: AuthenticatedUser, modifier: Modifier = Modifier) { - - val permissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) - val microphonePermissionState = - remember(permissionState.hasPermission) { mutableStateOf(permissionState.hasPermission) } - - val state = - remember(permissionState, microphonePermissionState) { - DefaultSpeechRecognitionScreenState(permissionState, microphonePermissionState) - } - - SpeechRecognitionScreen(state = state, modifier = modifier) -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt index 27ec77457..76db2eee8 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/home/HomeScaffold.kt @@ -26,8 +26,6 @@ enum class HomeSection( /** The section to manage our preferences. */ Settings(PawniesIcons.SectionSettings, { sectionSettings }), - /** The section to demonstrate Speech Recognition */ - SpeechRecognition(PawniesIcons.GameMicOn, { sectionSpeechRecognition }), } /** diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt index 6d2ffc6f4..e456942a4 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/English.kt @@ -84,7 +84,6 @@ object English : LocalizedStrings { override val sectionSocial = "Players" override val sectionSettings = "Settings" override val sectionPlay = "Play" - override val sectionSpeechRecognition = "Vocal" override val newGame = "New game".uppercase() diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt index e7f12d997..6ab50e676 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/i18n/LocalizedStrings.kt @@ -87,7 +87,6 @@ interface LocalizedStrings { val sectionSocial: String val sectionSettings: String val sectionPlay: String - val sectionSpeechRecognition: String val newGame: String val prepareGameChooseColor: String diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt deleted file mode 100644 index ca8665e89..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreen.kt +++ /dev/null @@ -1,126 +0,0 @@ -package ch.epfl.sdp.mobile.ui.speech_recognition - -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.MutatorMutex -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import ch.epfl.sdp.mobile.ui.* -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import kotlinx.coroutines.launch - -/* Extracted strings used for test, maybe removed later */ -const val PermissionGranted = "Permission has been granted ! " -const val PermissionDenied = "Permission was NOT GRANTED !" -const val DefaultText = "---" -const val ListeningText = "Listening..." -const val MicroIconDescription = "micro" - -/** Mutator used for cancelling a concurrent speech if the user mutes the mic */ -private val mutex = MutatorMutex() - -/** - * Screen for demonstrating the SpeechRecognition android feature - * @param state State of the screen - * @param recognizer [SpeechRecognizerEntity] entity used in speech recognition for this screen - * @param modifier [Modifier] of this composable - */ -@Composable -@OptIn(ExperimentalPermissionsApi::class) -fun SpeechRecognitionScreen( - state: SpeechRecognitionScreenState, - modifier: Modifier = Modifier, - recognizer: SpeechRecognizable = SpeechRecognizerEntity(), -) { - - val context = LocalContext.current - var text by remember { mutableStateOf(DefaultText) } - var activeSpeech by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - - val microIcon = if (activeSpeech) PawniesIcons.GameMicOn else PawniesIcons.GameMicOff - - /** - * Blocking function to ensure that only the most recent call to the block function is executed - * and older executions are cancelled - * - * @param block: a suspending function namely the speech recognition routine - */ - suspend fun vocalize(block: suspend () -> Unit) { - mutex.mutate(MutatePriority.UserInput) { - try { - block() - } finally { - activeSpeech = false - } - } - } - - Column( - verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier) { - - // Speech Text - Text(text, textAlign = TextAlign.Center) - // Microphone Button - OutlinedButton( - shape = CircleShape, - onClick = { - askForPermission(state.permissionState, state::onPermissionChange) - activeSpeech = !activeSpeech && state.hasMicrophonePermission - scope.launch { - vocalize { - if (activeSpeech) { - text = ListeningText - text = recognizer.recognition(context).joinToString(separator = "\n") - } else { - text = DefaultText - } - } - } - }) { Icon(microIcon, MicroIconDescription) } - - // Display information about vocal permission - PermissionText( - hasPermission = state.hasMicrophonePermission, - ) - } -} - -/** - * Asks the user to grant permission to use the mic if not already granted - * @param microPermissionState permission state of microphone - * @param onPermissionChange call back to change permission un the state - */ -@OptIn(ExperimentalPermissionsApi::class) -private fun askForPermission( - microPermissionState: PermissionState, - onPermissionChange: () -> Unit -) { - if (!microPermissionState.hasPermission) { - microPermissionState.launchPermissionRequest() - onPermissionChange() - } -} - -/** - * Composable responsible for displaying the permission text - * @param hasPermission True if the permission was granted false otherwise - * @param modifier [Modifier] for this composable - */ -@Composable -private fun PermissionText(modifier: Modifier = Modifier, hasPermission: Boolean = false) { - val text = if (hasPermission) PermissionGranted else PermissionDenied - Text(text = text, textAlign = TextAlign.Center, modifier = modifier) -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt deleted file mode 100644 index 46121ef48..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognitionScreenState.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ch.epfl.sdp.mobile.ui.speech_recognition - -import androidx.compose.runtime.Stable -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState - -/** State of the SpeechRecognition screen */ -@OptIn(ExperimentalPermissionsApi::class) -@Stable -interface SpeechRecognitionScreenState { - - /* PermissionState for microphone access */ - val permissionState: PermissionState - - /* Microphone right access */ - val hasMicrophonePermission: Boolean - - /* Call back when permission is changed */ - fun onPermissionChange(): Unit -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt deleted file mode 100644 index 64c863919..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizable.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ch.epfl.sdp.mobile.ui.speech_recognition - -import android.content.Context - -/** Interface ued to extract speech recognition routine */ -interface SpeechRecognizable { - /** - * Suspending function that returns list of speech - * @param context [Context] of app execution - * @return speech candidates - */ - suspend fun recognition(context: Context): List -} diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt deleted file mode 100644 index b93fb8c9c..000000000 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/speech_recognition/SpeechRecognizerEntity.kt +++ /dev/null @@ -1,65 +0,0 @@ -package ch.epfl.sdp.mobile.ui.speech_recognition - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.speech.RecognitionListener -import android.speech.RecognizerIntent -import android.speech.SpeechRecognizer -import kotlin.coroutines.resume -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine - -val defaultListener: (CancellableContinuation>) -> RecognitionListener = { cont -> - object : SpeechRecognizerEntity.RecognitionListenerAdapter() { - override fun onResults(results: Bundle?) { - super.onResults(results) - cont.resume( - // results cannot be null - results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) ?: emptyList()) - } - } -} - -class SpeechRecognizerEntity( - private val lang: String = "en-US", - private val maxResultsCount: Int = 10, - private val listener: (CancellableContinuation>) -> RecognitionListener = - defaultListener -) : SpeechRecognizable { - /** - * Returns speech results from the speech recognizer - * @param context [Context] context of the app execution - * @return List of size maximum [maxResultsCount] of speech recognizer results as strings - */ - override suspend fun recognition(context: Context): List = - suspendCancellableCoroutine { cont -> - val recognizer = SpeechRecognizer.createSpeechRecognizer(context) - val speechRecognizerIntent = - Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) // Speech action - .putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang) // Speech language - .putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResultsCount) // Number of results - // Listener for results - recognizer.setRecognitionListener(listener(cont)) - recognizer.startListening(speechRecognizerIntent) - - // Clearing upon coroutine cancellation - cont.invokeOnCancellation { - recognizer.stopListening() - recognizer.destroy() - } - } - - /** Adapter class for Recognition' listener */ - abstract class RecognitionListenerAdapter : RecognitionListener { - override fun onReadyForSpeech(params: Bundle?) = Unit - override fun onBeginningOfSpeech() = Unit - override fun onRmsChanged(rmsdB: Float) = Unit - override fun onBufferReceived(buffer: ByteArray?) = Unit - override fun onEndOfSpeech() = Unit - override fun onError(error: Int) = Unit - override fun onResults(results: Bundle?) = Unit - override fun onPartialResults(partialResults: Bundle?) = Unit - override fun onEvent(eventType: Int, params: Bundle?) = Unit - } -} From 4a8d811074c4fc35f1ebf9f7d26b9a2937c7d1db Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 18:45:18 +0200 Subject: [PATCH 29/42] Remove a comment --- mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt index 71a1d9bad..cfaa48083 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/ui/game/GameScreen.kt @@ -40,7 +40,6 @@ fun GameScreen( contentPadding: PaddingValues = PaddingValues(), snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ) { - // FIXME : This composable might require some refined handling of snackbar content padding. Scaffold( modifier = modifier, scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState), From 3678a4f3e5f86654b4c081a04ba575111bf176a2 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 20:05:43 +0200 Subject: [PATCH 30/42] Apply Ktfmt --- .../sdp/mobile/test/state/StatefulHomeTest.kt | 56 ++--- .../test/state/StatefulPlayScreenTest.kt | 206 +++++++++--------- .../test/state/StatefulSettingsScreenTest.kt | 48 ++-- 3 files changed, 155 insertions(+), 155 deletions(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt index 2e448a25b..37fe97914 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulHomeTest.kt @@ -269,35 +269,35 @@ class StatefulHomeTest { @Test fun given_statefulHome_when_creatingOnlineGameFromUI_then_gameScreenOpensWithCorrectOpponent() = runTest { - val auth = emptyAuth() - val store = buildStore { - collection("users") { document("userId2", ProfileDocument(name = "user2")) } + val auth = emptyAuth() + val store = buildStore { + collection("users") { document("userId2", ProfileDocument(name = "user2")) } + } + val authFacade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + authFacade.signUpWithEmail("user1@email", "user1", "password") + val currentUser = authFacade.currentUser.filterIsInstance().first() + val user2 = + social.profile(uid = "userId2", user = currentUser).filterIsInstance().first() + currentUser.follow(user2) + + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } } - val authFacade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) - - authFacade.signUpWithEmail("user1@email", "user1", "password") - val currentUser = authFacade.currentUser.filterIsInstance().first() - val user2 = - social.profile(uid = "userId2", user = currentUser).filterIsInstance().first() - currentUser.follow(user2) - - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, social, chess, speech) { StatefulHome(currentUser) } - } - - rule.onNodeWithText(strings.sectionPlay).performClick() - rule.onNodeWithText(strings.newGame).performClick() - rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() - rule.onNodeWithText("user2").performClick() - rule.onNodeWithText(strings.prepareGamePlay).performClick() - - rule.onNodeWithContentDescription(strings.boardContentDescription).assertExists() - rule.onNodeWithText("user2").assertExists() - } + + rule.onNodeWithText(strings.sectionPlay).performClick() + rule.onNodeWithText(strings.newGame).performClick() + rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() + rule.onNodeWithText("user2").performClick() + rule.onNodeWithText(strings.prepareGamePlay).performClick() + + rule.onNodeWithContentDescription(strings.boardContentDescription).assertExists() + rule.onNodeWithText("user2").assertExists() + } @Test fun given_statefulHome_when_creatingLocalGameFromUI_then_gameScreenOpens() = runTest { diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt index 9e792359d..1832cb89f 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulPlayScreenTest.kt @@ -68,87 +68,87 @@ class StatefulPlayScreenTest { @Test fun given_playerHasLostByCheckmate_when_accessingPlayScreen_then_displayLostByCheckmate() = runTest { - val auth = buildAuth { user("email@example.org", "password", "1") } - val store = buildStore { - collection("users") { - document("1", ProfileDocument("1")) - document("2", ProfileDocument("2", name = "test")) - } - collection("games") { - document( - /* Funfact: Fool's Mate, Fastest checkmate possible https://www.chess.com/article/view/fastest-chess-checkmates */ - "id", - ChessDocument( - uid = "786", - whiteId = "1", - blackId = "2", - moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) + val auth = buildAuth { user("email@example.org", "password", "1") } + val store = buildStore { + collection("users") { + document("1", ProfileDocument("1")) + document("2", ProfileDocument("2", name = "test")) + } + collection("games") { + document( + /* Funfact: Fool's Mate, Fastest checkmate possible https://www.chess.com/article/view/fastest-chess-checkmates */ + "id", + ChessDocument( + uid = "786", + whiteId = "1", + blackId = "2", + moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) + } + } + + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signInWithEmail("email@example.org", "password") + val userAuthenticated = facade.currentUser.filterIsInstance().first() + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = userAuthenticated, + onGameItemClick = {}, + navigateToPrepareGame = {}, + navigateToLocalGame = {}, + ) } } - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) - - facade.signInWithEmail("email@example.org", "password") - val userAuthenticated = facade.currentUser.filterIsInstance().first() - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess, speech) { - StatefulPlayScreen( - user = userAuthenticated, - onGameItemClick = {}, - navigateToPrepareGame = {}, - navigateToLocalGame = {}, - ) - } - } - - rule.onNodeWithText(strings.profileLostByCheckmate(4)).assertExists() - } + rule.onNodeWithText(strings.profileLostByCheckmate(4)).assertExists() + } @Test fun given_playerHasWonByCheckmate_when_accessingPlayScreen_then_displayWinByCheckmate() = runTest { - val auth = buildAuth { user("email@example.org", "password", "1") } - val store = buildStore { - collection("users") { - document("1", ProfileDocument("1")) - document("2", ProfileDocument("2", name = "test")) - } - collection("games") { - document( - "id", - ChessDocument( - uid = "786", - whiteId = "2", - blackId = "1", - moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) + val auth = buildAuth { user("email@example.org", "password", "1") } + val store = buildStore { + collection("users") { + document("1", ProfileDocument("1")) + document("2", ProfileDocument("2", name = "test")) + } + collection("games") { + document( + "id", + ChessDocument( + uid = "786", + whiteId = "2", + blackId = "1", + moves = listOf("f2-f3", "e7-e6", "g2-g4", "Qd8-h4"))) + } + } + + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signInWithEmail("email@example.org", "password") + val userAuthenticated = facade.currentUser.filterIsInstance().first() + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = userAuthenticated, + onGameItemClick = {}, + navigateToPrepareGame = {}, + navigateToLocalGame = {}, + ) } } - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) - - facade.signInWithEmail("email@example.org", "password") - val userAuthenticated = facade.currentUser.filterIsInstance().first() - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess, speech) { - StatefulPlayScreen( - user = userAuthenticated, - onGameItemClick = {}, - navigateToPrepareGame = {}, - navigateToLocalGame = {}, - ) - } - } - - rule.onNodeWithText(strings.profileWonByCheckmate(4)).assertExists() - } + rule.onNodeWithText(strings.profileWonByCheckmate(4)).assertExists() + } @Test fun statefulPlayScreen_isDisplayedWithNoWhiteId() = runTest { @@ -308,37 +308,37 @@ class StatefulPlayScreenTest { @Test fun given_playScreen_when_clickingNewGameAndOnlinePlay_then_onlineGameCallbackIsCalled() = runTest { - val auth = emptyAuth() - val store = buildStore { - collection("users") { document("userId2", ProfileDocument(name = "user2")) } + val auth = emptyAuth() + val store = buildStore { + collection("users") { document("userId2", ProfileDocument(name = "user2")) } + } + val facade = AuthenticationFacade(auth, store) + val social = SocialFacade(auth, store) + val chess = ChessFacade(auth, store) + val speech = SpeechFacade(FailingSpeechRecognizerFactory) + + facade.signUpWithEmail("user1@email", "user1", "password") + val currentUser = facade.currentUser.filterIsInstance().first() + + val channel = Channel(capacity = 1) + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(facade, social, chess, speech) { + StatefulPlayScreen( + user = currentUser, + onGameItemClick = {}, + navigateToPrepareGame = { + channel.trySend(Unit) + channel.close() + }, + navigateToLocalGame = {}, + ) + } } - val facade = AuthenticationFacade(auth, store) - val social = SocialFacade(auth, store) - val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) - - facade.signUpWithEmail("user1@email", "user1", "password") - val currentUser = facade.currentUser.filterIsInstance().first() - - val channel = Channel(capacity = 1) - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(facade, social, chess, speech) { - StatefulPlayScreen( - user = currentUser, - onGameItemClick = {}, - navigateToPrepareGame = { - channel.trySend(Unit) - channel.close() - }, - navigateToLocalGame = {}, - ) - } - } - - rule.onNodeWithText(strings.newGame).performClick() - rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() - - Truth.assertThat(channel.tryReceive().getOrNull()).isEqualTo(Unit) - } + + rule.onNodeWithText(strings.newGame).performClick() + rule.onNodeWithText(strings.prepareGamePlayOnline).performClick() + + Truth.assertThat(channel.tryReceive().getOrNull()).isEqualTo(Unit) + } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt index 1fc733561..547bb99d4 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt @@ -66,34 +66,34 @@ class StatefulSettingsScreenTest { @Test fun given_SettingScreenLoaded_when_clickingOnEditProfileName_then_functionShouldBeCalled() = runTest { - val user = mockk() - every { user.name } returns "test" - every { user.email } returns "test" - every { user.emoji } returns "test" - every { user.backgroundColor } returns Profile.Color.Orange - every { user.uid } returns "test" - every { user.followed } returns false - var functionCalled = false + val user = mockk() + every { user.name } returns "test" + every { user.email } returns "test" + every { user.emoji } returns "test" + every { user.backgroundColor } returns Profile.Color.Orange + every { user.uid } returns "test" + every { user.followed } returns false + var functionCalled = false - val openProfileEditNameMock = { functionCalled = true } + val openProfileEditNameMock = { functionCalled = true } - val auth = emptyAuth() - val store = emptyStore() + val auth = emptyAuth() + val store = emptyStore() - val authFacade = AuthenticationFacade(auth, store) - val socialFacade = SocialFacade(auth, store) - val chessFacade = ChessFacade(auth, store) - val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) + val authFacade = AuthenticationFacade(auth, store) + val socialFacade = SocialFacade(auth, store) + val chessFacade = ChessFacade(auth, store) + val speechFacade = SpeechFacade(FailingSpeechRecognizerFactory) - val strings = - rule.setContentWithLocalizedStrings { - ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { - StatefulSettingsScreen(user, openProfileEditNameMock, {}) - } - } + val strings = + rule.setContentWithLocalizedStrings { + ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { + StatefulSettingsScreen(user, openProfileEditNameMock, {}) + } + } - rule.onNodeWithContentDescription(strings.profileEditNameIcon).performClick() + rule.onNodeWithContentDescription(strings.profileEditNameIcon).performClick() - Truth.assertThat(functionCalled).isTrue() - } + Truth.assertThat(functionCalled).isTrue() + } } From 4ea78562518c8d155bf1706c0df5c4dd97fda81c Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 22:32:15 +0200 Subject: [PATCH 31/42] Add some tests for `AndroidSpeechRecognizerFactory` and `AndroidSpeechRecognizer` --- .../AndroidSpeechRecognizerFactoryTest.kt | 61 +++++++++++++++++++ .../android/RecognitionListenerAdapterTest.kt | 25 ++++++++ .../test/state/StatefulGameScreenTest.kt | 4 ++ .../android/AndroidSpeechRecognizerFactory.kt | 4 +- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/RecognitionListenerAdapterTest.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt new file mode 100644 index 000000000..3bfb51685 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt @@ -0,0 +1,61 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech.android + +import android.content.Context +import android.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizerFactory +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.junit.Test + +class AndroidSpeechRecognizerFactoryTest { + + @Test + fun given_factory_when_createSpeechRecognizer_then_createsFrameworkSpeechRecognizer() { + val context = mockk() + val factory = AndroidSpeechRecognizerFactory(context) + val recognizer = mockk() + mockkStatic(SpeechRecognizer::class) + + every { SpeechRecognizer.createSpeechRecognizer(context) } returns recognizer + + factory.createSpeechRecognizer() + + verify { SpeechRecognizer.createSpeechRecognizer(context) } + } + + @Test + fun given_recognizer_when_destroy_then_destroysFramework() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.destroy() } returns Unit + + recognizer.destroy() + + verify { framework.destroy() } + } + + @Test + fun given_recognizer_when_startListening_then_startsFrameworkListening() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.startListening(any()) } returns Unit + + recognizer.startListening() + + verify { framework.startListening(any()) } + } + + @Test + fun given_recognizer_when_stopListening_then_stopsFrameworkListening() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.stopListening() } returns Unit + + recognizer.stopListening() + + verify { framework.stopListening() } + } +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/RecognitionListenerAdapterTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/RecognitionListenerAdapterTest.kt new file mode 100644 index 000000000..bb383de56 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/RecognitionListenerAdapterTest.kt @@ -0,0 +1,25 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech.android + +import android.os.Bundle +import ch.epfl.sdp.mobile.infrastructure.speech.android.RecognitionListenerAdapter +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class RecognitionListenerAdapterTest { + + @Test + fun given_emptyAdapter_when_callingAnyMethod_then_neverThrowsAndAlwaysReturnsUnit() { + // This is slightly artificial, but we don't really expect anything else from the subject under + // test and it's required for code coverage. + val adapter = object : RecognitionListenerAdapter() {} + + assertThat(adapter.onReadyForSpeech(Bundle.EMPTY)).isEqualTo(Unit) + assertThat(adapter.onBeginningOfSpeech()).isEqualTo(Unit) + assertThat(adapter.onRmsChanged(0f)).isEqualTo(Unit) + assertThat(adapter.onBufferReceived(byteArrayOf())).isEqualTo(Unit) + assertThat(adapter.onEndOfSpeech()).isEqualTo(Unit) + assertThat(adapter.onResults(Bundle.EMPTY)).isEqualTo(Unit) + assertThat(adapter.onPartialResults(Bundle.EMPTY)).isEqualTo(Unit) + assertThat(adapter.onEvent(0, Bundle.EMPTY)).isEqualTo(Unit) + } +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 10736763f..7f9bbde78 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -2,8 +2,10 @@ package ch.epfl.sdp.mobile.test.state +import android.Manifest.permission.RECORD_AUDIO import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.rule.GrantPermissionRule import ch.epfl.sdp.mobile.application.ChessDocument import ch.epfl.sdp.mobile.application.ProfileDocument import ch.epfl.sdp.mobile.application.authentication.AuthenticatedUser @@ -604,6 +606,8 @@ class StatefulGameScreenTest { robot.assertHasPiece(4, 1, Black, Pawn) } + @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(RECORD_AUDIO) + @Test fun clickingListening_showsListeningText() { val robot = emptyGameAgainstOneselfRobot() diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt index 2cf1c5f5f..08b49f9aa 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt @@ -45,8 +45,8 @@ class AndroidSpeechRecognizerFactory( */ class AndroidSpeechRecognizer( private val recognizer: NativeSpeechRecognizer, - private val language: String, - private val resultsCount: Int, + private val language: String = DefaultLanguage, + private val resultsCount: Int = DefaultResultsCount, ) : SpeechRecognizer { override fun setListener(listener: SpeechRecognizer.Listener) = From 25fa5fe7243555dc5e220f2b0ada4c300e3d9b31 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 22:50:45 +0200 Subject: [PATCH 32/42] Add some tests for `SpeechFacad` --- .../test/application/SpeechFacadeTest.kt | 37 +++++++++++++++++++ .../speech/FailingSpeechRecognizerFactory.kt | 4 +- .../SuccessfulSpeechRecognizerFactory.kt | 29 +++++++++++++++ .../SuspendingSpeechRecognizerFactory.kt | 17 +++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/application/SpeechFacadeTest.kt create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuccessfulSpeechRecognizerFactory.kt create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuspendingSpeechRecognizerFactory.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/application/SpeechFacadeTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/application/SpeechFacadeTest.kt new file mode 100644 index 000000000..593b30b39 --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/application/SpeechFacadeTest.kt @@ -0,0 +1,37 @@ +package ch.epfl.sdp.mobile.test.application + +import ch.epfl.sdp.mobile.application.speech.SpeechFacade +import ch.epfl.sdp.mobile.application.speech.SpeechFacade.RecognitionResult.* +import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuccessfulSpeechRecognizer +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuccessfulSpeechRecognizerFactory +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuspendingSpeechRecognizerFactory +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SpeechFacadeTest { + + @Test + fun given_suspendingRecognizer_when_recognizesThenCancels_then_terminatesWithoutException() = + runTest(UnconfinedTestDispatcher()) { + val facade = SpeechFacade(SuspendingSpeechRecognizerFactory) + val job = launch { facade.recognize() } + job.cancelAndJoin() + } + + @Test + fun given_failingRecognizer_when_recognizes_then_returnsErrorInternal() = runTest { + val facade = SpeechFacade(FailingSpeechRecognizerFactory) + assertThat(facade.recognize()).isEqualTo(Failure.Internal) + } + + @Test + fun given_successfulRecognizer_when_recognizes_then_returnsResults() = runTest { + val facade = SpeechFacade(SuccessfulSpeechRecognizerFactory) + assertThat(facade.recognize()).isEqualTo(Success(SuccessfulSpeechRecognizer.Results)) + } +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt index 6ae371031..74ee09fdb 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/FailingSpeechRecognizerFactory.kt @@ -11,7 +11,9 @@ object FailingSpeechRecognizerFactory : SpeechRecognizerFactory { /** A [SpeechRecognizer] which will always fail to recognize the user input, and return an error. */ class FailingSpeechRecognizer : SpeechRecognizer { private var listener: SpeechRecognizer.Listener? = null - override fun setListener(listener: SpeechRecognizer.Listener) = Unit + override fun setListener(listener: SpeechRecognizer.Listener) { + this.listener = listener + } override fun startListening() { listener?.onError() } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuccessfulSpeechRecognizerFactory.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuccessfulSpeechRecognizerFactory.kt new file mode 100644 index 000000000..0f2f9840b --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuccessfulSpeechRecognizerFactory.kt @@ -0,0 +1,29 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech + +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory + +/** An implementation of [SpeechRecognizerFactory] which always succeeds. */ +object SuccessfulSpeechRecognizerFactory : SpeechRecognizerFactory { + override fun createSpeechRecognizer() = SuccessfulSpeechRecognizer() +} + +/** A [SpeechRecognizer] which always succeeds to recognize the user input, and return [Results]. */ +class SuccessfulSpeechRecognizer : SpeechRecognizer { + + companion object { + + /** The results which will always be returned on success. */ + val Results = listOf("Hello", "World") + } + + private var listener: SpeechRecognizer.Listener? = null + override fun setListener(listener: SpeechRecognizer.Listener) { + this.listener = listener + } + override fun startListening() { + listener?.onResults(Results) + } + override fun stopListening() = Unit + override fun destroy() = Unit +} diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuspendingSpeechRecognizerFactory.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuspendingSpeechRecognizerFactory.kt new file mode 100644 index 000000000..5dd5e1b7c --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/SuspendingSpeechRecognizerFactory.kt @@ -0,0 +1,17 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech + +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory + +/** An implementation of [SpeechRecognizerFactory] which never issues results. */ +object SuspendingSpeechRecognizerFactory : SpeechRecognizerFactory { + override fun createSpeechRecognizer(): SpeechRecognizer = SuspendingSpeechRecognizer +} + +/** A [SpeechRecognizer] which never returns. */ +object SuspendingSpeechRecognizer : SpeechRecognizer { + override fun setListener(listener: SpeechRecognizer.Listener) = Unit + override fun startListening() = Unit // Never triggers a result. + override fun stopListening() = Unit + override fun destroy() = Unit +} From fed7987e6e8e6a65165c79354da0dec4a7336b47 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 23:24:49 +0200 Subject: [PATCH 33/42] Add some tests to `StatefulGameScreen` --- mobile/build.gradle.kts | 1 + .../test/state/StatefulGameScreenTest.kt | 68 ++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 55d0f04fb..9a4801922 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -50,6 +50,7 @@ tasks.withType().configureEach { kotlinOptions.freeCompilerArgs += "-opt-in=androidx.compose.animation.ExperimentalAnimationApi" kotlinOptions.freeCompilerArgs += "-opt-in=androidx.compose.animation.core.ExperimentalTransitionApi" + kotlinOptions.freeCompilerArgs += "-Xjvm-default=compatibility" } dependencies { diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 7f9bbde78..4aeeeb6a8 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -14,6 +14,7 @@ import ch.epfl.sdp.mobile.application.chess.ChessFacade import ch.epfl.sdp.mobile.application.chess.engine.Rank import ch.epfl.sdp.mobile.application.social.SocialFacade import ch.epfl.sdp.mobile.application.speech.SpeechFacade +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizerFactory import ch.epfl.sdp.mobile.state.* import ch.epfl.sdp.mobile.state.game.MatchChessBoardState.Companion.toEngineRank import ch.epfl.sdp.mobile.state.game.MatchChessBoardState.Companion.toRank @@ -25,6 +26,9 @@ import ch.epfl.sdp.mobile.test.infrastructure.persistence.auth.emptyAuth import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.buildStore import ch.epfl.sdp.mobile.test.infrastructure.persistence.store.document import ch.epfl.sdp.mobile.test.infrastructure.speech.FailingSpeechRecognizerFactory +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuccessfulSpeechRecognizer +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuccessfulSpeechRecognizerFactory +import ch.epfl.sdp.mobile.test.infrastructure.speech.SuspendingSpeechRecognizerFactory import ch.epfl.sdp.mobile.test.ui.game.ChessBoardRobot import ch.epfl.sdp.mobile.test.ui.game.click import ch.epfl.sdp.mobile.test.ui.game.drag @@ -34,6 +38,7 @@ import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Color.Black import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Color.White import ch.epfl.sdp.mobile.ui.game.ChessBoardState.Rank.* import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState import com.google.common.truth.Truth.assertThat import io.mockk.every import io.mockk.mockk @@ -50,9 +55,13 @@ class StatefulGameScreenTest { * playing against himself * * @param actions the [StatefulGameScreenActions] for this composable. + * @param recognizer the [SpeechRecognizerFactory] used to make the speech request. + * @param audioPermission the [PermissionState] to access audio. */ private fun emptyGameAgainstOneselfRobot( actions: StatefulGameScreenActions = StatefulGameScreenActions(onBack = {}, onShowAr = {}), + recognizer: SpeechRecognizerFactory = SuspendingSpeechRecognizerFactory, + audioPermission: PermissionState = GrantedPermissionState, ): ChessBoardRobot { val auth = emptyAuth() val store = buildStore { @@ -65,7 +74,7 @@ class StatefulGameScreenTest { val authApi = AuthenticationFacade(auth, store) val social = SocialFacade(auth, store) val chess = ChessFacade(auth, store) - val speech = SpeechFacade(FailingSpeechRecognizerFactory) + val speech = SpeechFacade(recognizer) val user1 = mockk() every { user1.uid } returns "userId1" @@ -73,7 +82,7 @@ class StatefulGameScreenTest { val strings = rule.setContentWithLocalizedStrings { ProvideFacades(authApi, social, chess, speech) { - StatefulGameScreen(user1, "gameId", actions) + StatefulGameScreen(user1, "gameId", actions, audioPermissionState = audioPermission) } } @@ -693,4 +702,59 @@ class StatefulGameScreenTest { robot.onNodeWithContentDescription(robot.strings.boardPieceQueen).performClick() robot.onNodeWithLocalizedText { robot.strings.gamePromoteConfirm }.assertIsNotEnabled() } + + @Test + fun given_successfulRecognizer_when_clicksListening_then_displaysRecognitionResults() { + // This will be fail once we want to move the pieces instead. + val robot = + emptyGameAgainstOneselfRobot( + recognizer = SuccessfulSpeechRecognizerFactory, + audioPermission = GrantedPermissionState, + ) + robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.performClick() + robot.onNodeWithText(SuccessfulSpeechRecognizer.Results[0]).assertExists() + } + + @Test + fun given_failingRecognizer_when_clicksListening_then_displaysFailedRecognitionResults() { + // This will be fail once we want to move the pieces instead. + val robot = + emptyGameAgainstOneselfRobot( + recognizer = FailingSpeechRecognizerFactory, + audioPermission = GrantedPermissionState, + ) + robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.performClick() + robot.onNodeWithText("Internal failure").assertExists() + } + + @Test + fun given_noPermission_when_clicksListening_then_requestsPermission() { + val permission = MissingPermissionState() + val robot = + emptyGameAgainstOneselfRobot( + recognizer = SuspendingSpeechRecognizerFactory, + audioPermission = permission, + ) + robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.performClick() + assertThat(permission.permissionRequested).isTrue() + } +} + +private object GrantedPermissionState : PermissionState { + override val hasPermission = true + override val permission = RECORD_AUDIO + override val permissionRequested = true + override val shouldShowRationale = false + override fun launchPermissionRequest() = Unit +} + +private class MissingPermissionState : PermissionState { + override var permissionRequested = false + override val permission = RECORD_AUDIO + override val hasPermission + get() = permissionRequested + override val shouldShowRationale = false + override fun launchPermissionRequest() { + permissionRequested = true + } } From 971788844ce2889ca3b3ff4311a93f74aa88d29c Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 23:39:50 +0200 Subject: [PATCH 34/42] Improve coverage of `AndroidSpeechRecognizer` --- .../AndroidSpeechRecognizerFactoryTest.kt | 34 -------- .../android/AndroidSpeechRecognizerTest.kt | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt index 3bfb51685..b4de1545a 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerFactoryTest.kt @@ -2,7 +2,6 @@ package ch.epfl.sdp.mobile.test.infrastructure.speech.android import android.content.Context import android.speech.SpeechRecognizer -import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizer import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizerFactory import io.mockk.every import io.mockk.mockk @@ -25,37 +24,4 @@ class AndroidSpeechRecognizerFactoryTest { verify { SpeechRecognizer.createSpeechRecognizer(context) } } - - @Test - fun given_recognizer_when_destroy_then_destroysFramework() { - val framework = mockk() - val recognizer = AndroidSpeechRecognizer(framework) - every { framework.destroy() } returns Unit - - recognizer.destroy() - - verify { framework.destroy() } - } - - @Test - fun given_recognizer_when_startListening_then_startsFrameworkListening() { - val framework = mockk() - val recognizer = AndroidSpeechRecognizer(framework) - every { framework.startListening(any()) } returns Unit - - recognizer.startListening() - - verify { framework.startListening(any()) } - } - - @Test - fun given_recognizer_when_stopListening_then_stopsFrameworkListening() { - val framework = mockk() - val recognizer = AndroidSpeechRecognizer(framework) - every { framework.stopListening() } returns Unit - - recognizer.stopListening() - - verify { framework.stopListening() } - } } diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt new file mode 100644 index 000000000..259d5afad --- /dev/null +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt @@ -0,0 +1,83 @@ +package ch.epfl.sdp.mobile.test.infrastructure.speech.android + +import android.speech.RecognitionListener +import android.speech.SpeechRecognizer +import android.speech.SpeechRecognizer.RESULTS_RECOGNITION +import androidx.core.os.bundleOf +import ch.epfl.sdp.mobile.infrastructure.speech.SpeechRecognizer.Listener +import ch.epfl.sdp.mobile.infrastructure.speech.android.AndroidSpeechRecognizer +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class AndroidSpeechRecognizerTest { + + @Test + fun given_recognizer_when_destroy_then_destroysFramework() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.destroy() } returns Unit + + recognizer.destroy() + + verify { framework.destroy() } + } + + @Test + fun given_recognizer_when_startListening_then_startsFrameworkListening() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.startListening(any()) } returns Unit + + recognizer.startListening() + + verify { framework.startListening(any()) } + } + + @Test + fun given_recognizer_when_stopListening_then_stopsFrameworkListening() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + every { framework.stopListening() } returns Unit + + recognizer.stopListening() + + verify { framework.stopListening() } + } + + @Test + fun given_recognizer_when_settingListener_then_callsErrorListener() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + val listener = mockk() + + every { listener.onError() } returns Unit + every { framework.setRecognitionListener(any()) } answers + { + firstArg().onError(0) + } + + recognizer.setListener(listener) + + verify { listener.onError() } + } + + @Test + fun given_recognizer_when_settingListener_then_callsSuccessListener() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + val listener = mockk() + + every { listener.onResults(arrayListOf("Hello")) } returns Unit + every { framework.setRecognitionListener(any()) } answers + { + firstArg() + .onResults(bundleOf(RESULTS_RECOGNITION to arrayListOf("Hello"))) + } + + recognizer.setListener(listener) + + verify { listener.onResults(arrayListOf("Hello")) } + } +} From dbea3e2e8d15750216b18ee8189fabd874e217fb Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 23:43:15 +0200 Subject: [PATCH 35/42] Use `mutableStateOf` for `MissingPermissionState` --- .../ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 4aeeeb6a8..e9793f58d 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -3,6 +3,9 @@ package ch.epfl.sdp.mobile.test.state import android.Manifest.permission.RECORD_AUDIO +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.rule.GrantPermissionRule @@ -749,7 +752,7 @@ private object GrantedPermissionState : PermissionState { } private class MissingPermissionState : PermissionState { - override var permissionRequested = false + override var permissionRequested by mutableStateOf(false) override val permission = RECORD_AUDIO override val hasPermission get() = permissionRequested From cd1419b914b25c8116db3c7dc1f58bfad787b52a Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 23:51:42 +0200 Subject: [PATCH 36/42] Increase coverage of `AndroidSpeechRecognizer` --- .../android/AndroidSpeechRecognizerTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt index 259d5afad..08cddad36 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/infrastructure/speech/android/AndroidSpeechRecognizerTest.kt @@ -80,4 +80,21 @@ class AndroidSpeechRecognizerTest { verify { listener.onResults(arrayListOf("Hello")) } } + + @Test + fun given_recognizer_when_settingListener_then_callsSuccessListenerWithDefaultValue() { + val framework = mockk() + val recognizer = AndroidSpeechRecognizer(framework) + val listener = mockk() + + every { listener.onResults(emptyList()) } returns Unit + every { framework.setRecognitionListener(any()) } answers + { + firstArg().onResults(bundleOf(RESULTS_RECOGNITION to null)) + } + + recognizer.setListener(listener) + + verify { listener.onResults(emptyList()) } + } } From c4bea0be7a69942a7cc365a1668278ba905d77f7 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Wed, 4 May 2022 23:55:40 +0200 Subject: [PATCH 37/42] Add some missing documentation --- .../src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt index db3eee846..f66c4430a 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/CompositionLocals.kt @@ -27,6 +27,8 @@ val LocalSpeechFacade = compositionLocalOf { error("Missing Speech * * @param authentication the [AuthenticationFacade] that will be provided. * @param social the [SocialFacade] that will be provided. + * @param chess the [ChessFacade] that will be provided. + * @param speech the [SpeechFacade] that will be provided. */ @Composable fun ProvideFacades( From 288f267d8e281a4f4bc2bdb6c8accc6233e1d760 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Thu, 5 May 2022 13:28:15 +0200 Subject: [PATCH 38/42] Fix some documentation --- .../speech/android/AndroidSpeechRecognizerFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt index 08b49f9aa..11933f3f8 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/infrastructure/speech/android/AndroidSpeechRecognizerFactory.kt @@ -16,7 +16,7 @@ private const val DefaultLanguage = "en-US" private const val DefaultResultsCount = 10 /** - * An implementation of a [SpeechRecognizer] which is backed by a [NativeSpeechRecognizer]. + * An implementation of a [SpeechRecognizerFactory] which is backed by [NativeSpeechRecognizer]s. * * @param context the [Context] which will be used to create the speech recognizers. * @param language the default language to use. From b0095e96c7f9be668d304db6f55e466ef5cfb600 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Thu, 5 May 2022 14:02:51 +0200 Subject: [PATCH 39/42] Cancel speech recognition on click when listening --- .../sdp/mobile/test/state/StatefulGameScreenTest.kt | 12 ++++++++++++ .../ch/epfl/sdp/mobile/state/StatefulGameScreen.kt | 2 ++ 2 files changed, 14 insertions(+) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index e9793f58d..2f4bf8994 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -741,6 +741,18 @@ class StatefulGameScreenTest { robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.performClick() assertThat(permission.permissionRequested).isTrue() } + + @Test + fun given_suspendingRecognizer_when_clickingListeningTwice_then_cancelsRecognition() { + val robot = + emptyGameAgainstOneselfRobot( + recognizer = SuspendingSpeechRecognizerFactory, + audioPermission = GrantedPermissionState, + ) + robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.performClick() + robot.onNodeWithLocalizedText { gameListening }.performClick() + robot.onNodeWithLocalizedContentDescription { gameMicOffContentDescription }.assertExists() + } } private object GrantedPermissionState : PermissionState { diff --git a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt index 3f6d654d1..e877bee34 100644 --- a/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt +++ b/mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulGameScreen.kt @@ -142,8 +142,10 @@ constructor( override fun onListenClick() { scope.launch { + val willCancel = listening mutex.mutate(UserInput) { try { + if (willCancel) return@mutate listening = true if (!permission.hasPermission) { permission.launchPermissionRequest() From fdc83107e4b3834b31a94567753ca642c892d613 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Thu, 5 May 2022 16:28:06 +0200 Subject: [PATCH 40/42] Apply Ktfmt --- .../ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt index 99aab81b3..cf5e534f4 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulSettingsScreenTest.kt @@ -88,7 +88,7 @@ class StatefulSettingsScreenTest { val strings = rule.setContentWithLocalizedStrings { ProvideFacades(authFacade, socialFacade, chessFacade, speechFacade) { - StatefulSettingsScreen(user, {}, openProfileEditNameMock, {}) + StatefulSettingsScreen(user, {}, openProfileEditNameMock, {}) } } From 74da8d8b62674bfa769dce21aa7d72ad0e52a9e7 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Thu, 5 May 2022 16:37:21 +0200 Subject: [PATCH 41/42] Update mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt Co-authored-by: BadrTad <56789776+BadrTad@users.noreply.github.com> --- .../ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 2f4bf8994..06f1ce6e1 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -708,7 +708,7 @@ class StatefulGameScreenTest { @Test fun given_successfulRecognizer_when_clicksListening_then_displaysRecognitionResults() { - // This will be fail once we want to move the pieces instead. + // This will fail once we want to move the pieces instead. val robot = emptyGameAgainstOneselfRobot( recognizer = SuccessfulSpeechRecognizerFactory, From a83711122ac68550648a03da3beb367413302295 Mon Sep 17 00:00:00 2001 From: Alexandre Piveteau Date: Thu, 5 May 2022 16:37:28 +0200 Subject: [PATCH 42/42] Update mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt Co-authored-by: BadrTad <56789776+BadrTad@users.noreply.github.com> --- .../ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt index 06f1ce6e1..f627a8a0b 100644 --- a/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt +++ b/mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/StatefulGameScreenTest.kt @@ -720,7 +720,7 @@ class StatefulGameScreenTest { @Test fun given_failingRecognizer_when_clicksListening_then_displaysFailedRecognitionResults() { - // This will be fail once we want to move the pieces instead. + // This will fail once we want to move the pieces instead. val robot = emptyGameAgainstOneselfRobot( recognizer = FailingSpeechRecognizerFactory,