Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
package org.mozilla.fenix.components
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.speech.RecognizerIntent
import androidx.activity.result.ActivityResultLauncher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.R
import org.mozilla.fenix.components.appstate.VoiceSearchAction.VoiceInputRequestCleared
import org.mozilla.fenix.components.appstate.VoiceSearchAction.VoiceInputResultReceived
/**
* A generic feature for handling voice search requests and results.
* - Observes voice search requests from the AppStore.
* - Launches voice recognition and updates AppStore with the result.
* - Does NOT update any UI or toolbar state directly.
*
* Other features (such as toolbar middleware) should observe AppStore for
* voice search results and update their own state accordingly.
*/
class VoiceSearchFeature(
private val context: Context,
private val appStore: AppStore,
private val voiceSearchLauncher: ActivityResultLauncher<Intent>,
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
override fun start() {
observeVoiceSearchRequests()
}
override fun stop() {
scope?.cancel()
scope = null
}
private fun observeVoiceSearchRequests() {
scope = appStore.flowScoped { flow ->
flow.map { state -> state.voiceSearchState }
.distinctUntilChangedBy { it.isRequestingVoiceInput }
.collect { voiceSearchState ->
if (voiceSearchState.isRequestingVoiceInput) {
appStore.dispatch(VoiceInputRequestCleared)
launchVoiceSearch()
}
}
}
}
private fun launchVoiceSearch() {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PROMPT, context.getString(R.string.voice_search_explainer))
}
try {
voiceSearchLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
appStore.dispatch(VoiceInputResultReceived(null))
} catch (e: SecurityException) {
appStore.dispatch(VoiceInputResultReceived(null))
}
}
/**
* Handles the voice search result. This should be called from the ActivityResultLauncher callback.
*/
fun handleVoiceSearchResult(resultCode: Int, data: Intent?) {
val searchTerms = if (resultCode == Activity.RESULT_OK && data != null) {
data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.firstOrNull()
} else {
null
}
appStore.dispatch(VoiceInputResultReceived(searchTerms))
}
}