This commit is contained in:
4 files changed
+173
-27
No files matched your search
@@ -13,6 +13,12 @@
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".HermesControlApp"
|
||||
android:allowBackup="false"
|
||||
|
||||
@@ -2,12 +2,12 @@ package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.speech.RecognizerIntent
|
||||
import android.speech.SpeechRecognizer
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -343,21 +343,18 @@ fun ChatScreen(
|
||||
) { granted ->
|
||||
ExternalActivityLifecycleGuard.externalActivityReturned()
|
||||
if (granted) {
|
||||
if (SpeechRecognizer.isRecognitionAvailable(context)) {
|
||||
val intent =
|
||||
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
|
||||
)
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_PROMPT,
|
||||
micListeningPrompt,
|
||||
)
|
||||
}
|
||||
if (SpeechInputHelper.isSpeechInputAvailable(context)) {
|
||||
val intent = SpeechInputHelper.createSpeechIntent(micListeningPrompt)
|
||||
isListening = true
|
||||
launchExternalActivity {
|
||||
speechLauncher.launch(intent)
|
||||
try {
|
||||
speechLauncher.launch(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
isListening = false
|
||||
scrollScope.launch {
|
||||
snackbarHostState.showSnackbar(sttNotAvailableMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollScope.launch {
|
||||
@@ -781,21 +778,18 @@ fun ChatScreen(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
if (SpeechRecognizer.isRecognitionAvailable(context)) {
|
||||
val intent =
|
||||
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
|
||||
)
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_PROMPT,
|
||||
micListeningPrompt,
|
||||
)
|
||||
}
|
||||
if (SpeechInputHelper.isSpeechInputAvailable(context)) {
|
||||
val intent = SpeechInputHelper.createSpeechIntent(micListeningPrompt)
|
||||
isListening = true
|
||||
launchExternalActivity {
|
||||
speechLauncher.launch(intent)
|
||||
try {
|
||||
speechLauncher.launch(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
isListening = false
|
||||
scrollScope.launch {
|
||||
snackbarHostState.showSnackbar(sttNotAvailableMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollScope.launch {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.speech.RecognizerIntent
|
||||
import android.speech.SpeechRecognizer
|
||||
|
||||
object SpeechInputHelper {
|
||||
/**
|
||||
* Checks if speech recognition is available on this device through any registered provider,
|
||||
* including standard Android [SpeechRecognizer] services or offline/third-party apps
|
||||
* that handle [RecognizerIntent.ACTION_RECOGNIZE_SPEECH] (e.g. FUTO Voice Input).
|
||||
*/
|
||||
fun isSpeechInputAvailable(
|
||||
context: Context,
|
||||
isRecognizerServiceAvailable: (Context) -> Boolean = { ctx ->
|
||||
runCatching { SpeechRecognizer.isRecognitionAvailable(ctx) }.getOrDefault(false)
|
||||
},
|
||||
): Boolean {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
val activities =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
runCatching {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.ResolveInfoFlags.of(0L),
|
||||
)
|
||||
}.getOrDefault(emptyList())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
runCatching {
|
||||
context.packageManager.queryIntentActivities(intent, 0)
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
return activities.isNotEmpty() || isRecognizerServiceAvailable(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the standard speech recognition intent with language model and prompt.
|
||||
*/
|
||||
fun createSpeechIntent(prompt: String): Intent =
|
||||
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
|
||||
)
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_PROMPT,
|
||||
prompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.unmockkConstructor
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SpeechInputHelperTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkConstructor(Intent::class)
|
||||
every { anyConstructed<Intent>().setAction(any()) } answers { self as Intent }
|
||||
every { anyConstructed<Intent>().putExtra(any<String>(), any<String>()) } answers { self as Intent }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkConstructor(Intent::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSpeechInputAvailable_returnsTrue_whenPackageManagerFindsActivities() {
|
||||
val context = mockk<Context>()
|
||||
val packageManager = mockk<PackageManager>()
|
||||
val resolveInfo = mockk<ResolveInfo>()
|
||||
|
||||
every { context.packageManager } returns packageManager
|
||||
every {
|
||||
packageManager.queryIntentActivities(any<Intent>(), any<Int>())
|
||||
} returns listOf(resolveInfo)
|
||||
|
||||
val result =
|
||||
SpeechInputHelper.isSpeechInputAvailable(
|
||||
context = context,
|
||||
isRecognizerServiceAvailable = { false },
|
||||
)
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSpeechInputAvailable_returnsTrue_whenRecognitionServiceAvailableEvenIfNoActivities() {
|
||||
val context = mockk<Context>()
|
||||
val packageManager = mockk<PackageManager>()
|
||||
|
||||
every { context.packageManager } returns packageManager
|
||||
every {
|
||||
packageManager.queryIntentActivities(any<Intent>(), any<Int>())
|
||||
} returns emptyList()
|
||||
|
||||
val result =
|
||||
SpeechInputHelper.isSpeechInputAvailable(
|
||||
context = context,
|
||||
isRecognizerServiceAvailable = { true },
|
||||
)
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSpeechInputAvailable_returnsFalse_whenNoActivitiesAndNoRecognitionService() {
|
||||
val context = mockk<Context>()
|
||||
val packageManager = mockk<PackageManager>()
|
||||
|
||||
every { context.packageManager } returns packageManager
|
||||
every {
|
||||
packageManager.queryIntentActivities(any<Intent>(), any<Int>())
|
||||
} returns emptyList()
|
||||
|
||||
val result =
|
||||
SpeechInputHelper.isSpeechInputAvailable(
|
||||
context = context,
|
||||
isRecognizerServiceAvailable = { false },
|
||||
)
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createSpeechIntent_constructsIntentWithActionAndPrompt() {
|
||||
val prompt = "Listening..."
|
||||
val intent = SpeechInputHelper.createSpeechIntent(prompt)
|
||||
assertNotNull(intent)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user