Skip to content
This repository has been archived by the owner on Jul 7, 2022. It is now read-only.

Use speech recognition from the game screen #301

Merged
merged 56 commits into from May 5, 2022
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2ee06e8
Start a SpeechRecognizer screen.
BadrTad Apr 1, 2022
49b2133
Merge branch 'main' into feature/bt/speech_recognition
BadrTad Apr 1, 2022
c79fb6b
Format
BadrTad Apr 1, 2022
b996de7
Merge remote-tracking branch 'origin/feature/bt/speech_recognition' i…
BadrTad Apr 1, 2022
3b303b0
First working version of speech recognizer
BadrTad Apr 1, 2022
5701942
Increase Number of results to 10
BadrTad Apr 1, 2022
4deaafa
Merge branch 'main' into feature/bt/speech_recognition
BadrTad Apr 1, 2022
613caae
Fix toml by removing duplicate accompanist value
BadrTad Apr 1, 2022
881d8bc
Resolve comments:
BadrTad Apr 3, 2022
7385052
Merge branch 'main' into feature/bt/speech_recognition
BadrTad Apr 3, 2022
567dde9
Use already defined mic icons
BadrTad Apr 3, 2022
fe6d605
Merge remote-tracking branch 'origin/feature/bt/speech_recognition' i…
BadrTad Apr 3, 2022
ca558c2
Use already defined mic icons
BadrTad Apr 3, 2022
7392e24
Update mobile/src/main/java/ch/epfl/sdp/mobile/state/StatefulHome.kt
BadrTad Apr 3, 2022
4ab1fe3
Make constants private
BadrTad Apr 4, 2022
184455d
Add tests and revise permissions in manifest
BadrTad Apr 5, 2022
c40fb01
Pass modifier from state screen to screen
BadrTad Apr 5, 2022
9ff589e
Reformat
BadrTad Apr 5, 2022
36fd039
Add tests, doc and refactor
BadrTad Apr 7, 2022
40031fc
Format
BadrTad Apr 7, 2022
6599ac7
Merge branch 'main' into feature/bt/speech_recognition
BadrTad Apr 7, 2022
1fc3521
Fix test by changing permission to Record audio
BadrTad Apr 7, 2022
1a18299
Merge remote-tracking branch 'origin/feature/bt/speech_recognition' i…
BadrTad Apr 7, 2022
f302b30
Refresh CI
BadrTad Apr 7, 2022
a8a3764
Merge branch 'main' into feature/bt/speech_recognition
BadrTad Apr 7, 2022
6edbe7e
Add more tests to increase coverage
BadrTad Apr 7, 2022
c467ade
Merge remote-tracking branch 'origin/feature/bt/speech_recognition' i…
BadrTad Apr 7, 2022
2246da6
Refresh Cysrus
BadrTad Apr 7, 2022
87ca37b
Refactor code and make classes testable
BadrTad Apr 26, 2022
9f11201
Add test comments
BadrTad Apr 26, 2022
93c62d5
Remove extra from intent
BadrTad Apr 26, 2022
1ac978c
Add broken intent test with comments
BadrTad Apr 28, 2022
8e577de
Add unstable tests
BadrTad May 3, 2022
5b5eefa
Refactor added tests
BadrTad May 3, 2022
58152af
Merge branch 'main' into feature/bt/speech_recognition
BadrTad May 3, 2022
63cadca
Fix merge build
BadrTad May 3, 2022
10ab029
Merge branch 'main' into feature/bt/speech_recognition
BadrTad May 3, 2022
b3a88b7
Merge branch 'main' into feature/bt/speech_recognition
BadrTad May 3, 2022
acf5c89
Introduce `SpeechFacade`, `SpeechRecognizerFactory` and `SpeechRecogn…
alexandrepiveteau May 4, 2022
7a484aa
Merge remote-tracking branch 'origin/main' into feature/ap-bt/speech_…
alexandrepiveteau May 4, 2022
dabe0f5
Remove some legacy code
alexandrepiveteau May 4, 2022
4a8d811
Remove a comment
alexandrepiveteau May 4, 2022
3678a4f
Apply Ktfmt
alexandrepiveteau May 4, 2022
4ea7856
Add some tests for `AndroidSpeechRecognizerFactory` and `AndroidSpeec…
alexandrepiveteau May 4, 2022
25fa5fe
Add some tests for `SpeechFacad`
alexandrepiveteau May 4, 2022
fed7987
Add some tests to `StatefulGameScreen`
alexandrepiveteau May 4, 2022
9717888
Improve coverage of `AndroidSpeechRecognizer`
alexandrepiveteau May 4, 2022
dbea3e2
Use `mutableStateOf` for `MissingPermissionState`
alexandrepiveteau May 4, 2022
cd1419b
Increase coverage of `AndroidSpeechRecognizer`
alexandrepiveteau May 4, 2022
c4bea0b
Add some missing documentation
alexandrepiveteau May 4, 2022
288f267
Fix some documentation
alexandrepiveteau May 5, 2022
b0095e9
Cancel speech recognition on click when listening
alexandrepiveteau May 5, 2022
e9ae065
Merge branch 'main' into feature/ap-bt/speech_recognition
alexandrepiveteau May 5, 2022
fdc8310
Apply Ktfmt
alexandrepiveteau May 5, 2022
74da8d8
Update mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/Stat…
alexandrepiveteau May 5, 2022
a837111
Update mobile/src/androidTest/java/ch/epfl/sdp/mobile/test/state/Stat…
alexandrepiveteau May 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Expand Up @@ -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" }
Expand Down Expand Up @@ -59,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",
Expand Down
1 change: 1 addition & 0 deletions mobile/build.gradle.kts
Expand Up @@ -50,6 +50,7 @@ tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=androidx.compose.animation.ExperimentalAnimationApi"
kotlinOptions.freeCompilerArgs +=
"-opt-in=androidx.compose.animation.core.ExperimentalTransitionApi"
kotlinOptions.freeCompilerArgs += "-Xjvm-default=compatibility"
}

dependencies {
Expand Down
@@ -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))
}
}
@@ -0,0 +1,22 @@
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) {
this.listener = listener
}
override fun startListening() {
listener?.onError()
}
override fun stopListening() = Unit
override fun destroy() = Unit
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,27 @@
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.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<Context>()
val factory = AndroidSpeechRecognizerFactory(context)
val recognizer = mockk<SpeechRecognizer>()
mockkStatic(SpeechRecognizer::class)

every { SpeechRecognizer.createSpeechRecognizer(context) } returns recognizer

factory.createSpeechRecognizer()

verify { SpeechRecognizer.createSpeechRecognizer(context) }
}
}
@@ -0,0 +1,100 @@
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 {
alexandrepiveteau marked this conversation as resolved.
Show resolved Hide resolved

@Test
fun given_recognizer_when_destroy_then_destroysFramework() {
val framework = mockk<SpeechRecognizer>()
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<SpeechRecognizer>()
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<SpeechRecognizer>()
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<SpeechRecognizer>()
val recognizer = AndroidSpeechRecognizer(framework)
val listener = mockk<Listener>()

every { listener.onError() } returns Unit
every { framework.setRecognitionListener(any()) } answers
{
firstArg<RecognitionListener>().onError(0)
}

recognizer.setListener(listener)

verify { listener.onError() }
}

@Test
fun given_recognizer_when_settingListener_then_callsSuccessListener() {
val framework = mockk<SpeechRecognizer>()
val recognizer = AndroidSpeechRecognizer(framework)
val listener = mockk<Listener>()

every { listener.onResults(arrayListOf("Hello")) } returns Unit
every { framework.setRecognitionListener(any()) } answers
{
firstArg<RecognitionListener>()
.onResults(bundleOf(RESULTS_RECOGNITION to arrayListOf("Hello")))
}

recognizer.setListener(listener)

verify { listener.onResults(arrayListOf("Hello")) }
}

@Test
fun given_recognizer_when_settingListener_then_callsSuccessListenerWithDefaultValue() {
val framework = mockk<SpeechRecognizer>()
val recognizer = AndroidSpeechRecognizer(framework)
val listener = mockk<Listener>()

every { listener.onResults(emptyList()) } returns Unit
every { framework.setRecognitionListener(any()) } answers
{
firstArg<RecognitionListener>().onResults(bundleOf(RESULTS_RECOGNITION to null))
}

recognizer.setListener(listener)

verify { listener.onResults(emptyList()) }
}
}
@@ -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)
}
}
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -40,7 +46,7 @@ class ClassicChessBoardStateTest {
val match = facade.createMatch(user, user)

val actions = StatefulGameScreenActions(onBack = {}, onShowAr = {})
val state = MatchGameScreenState(actions, user, match, scope)
val state = MatchGameScreenState(actions, user, match, scope, NoOpSpeechRecognizerState)

state.onPositionClick(ChessBoardState.Position(4, 6))
assertThat(state.availableMoves)
Expand Down
Expand Up @@ -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
Expand All @@ -30,4 +31,11 @@ class CompositionLocalsTest {
rule.setContent { LocalSocialFacade.current }
}
}

@Test
fun missingSpeechFacade_throwsException() {
assertThrows(IllegalStateException::class.java) {
rule.setContent { LocalSpeechFacade.current }
}
}
}
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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 ?
Expand All @@ -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")

Expand All @@ -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")

Expand Down