feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person chose in Android, and puts the text it returns where the cursor stood, as Dictation does; cancelled, nothing. Only where none is installed, or it can't be opened, does speech_to_text listen in line as before. halkyon_app: VoiceInputApp (one listen, an answer once closed) on the channel app.halkyon/voice_input, which DeviceSpeechInput asks first at every listen, answering SpeechStart.finished so that Dictation doesn't listen again; speech_to_text isn't touched on that route, so our app asks for no microphone. SpeechInput.listen and Dictation.start take the prompt the voice input app shows (the field's hint). Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult, the first of EXTRA_RESULTS) attached in MainActivity, which hands it activity results, and RECOGNIZE_SPEECH among the manifest's queries. The privacy pages of Learn and Meditate say so. Our own Whisper and Meditate's dream recordings are dropped (D18). Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input. Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4940230ef1
commit
c36bd83c6a
3 changed files with 827 additions and 27 deletions
|
|
@ -96,5 +96,10 @@
|
|||
<intent>
|
||||
<action android:name="android.speech.RecognitionService"/>
|
||||
</intent>
|
||||
<!-- The voice input app dictation opens first (Meditate's ADR 0038, D18),
|
||||
FUTO Voice Input or Google's, which records itself. -->
|
||||
<intent>
|
||||
<action android:name="android.speech.action.RECOGNIZE_SPEECH"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import io.flutter.plugin.common.EventChannel
|
|||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
/** The voice input app dictation opens (ADR 0038, D18). */
|
||||
private val voiceInput = VoiceInput(this)
|
||||
|
||||
private var askedToNotify = false
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
|
|
@ -21,6 +24,7 @@ class MainActivity : FlutterActivity() {
|
|||
PomodoroService.appBack()
|
||||
ScreenGuard.attach(this, messenger)
|
||||
SensitiveClipboard.attach(this, messenger)
|
||||
voiceInput.attach(messenger)
|
||||
// A question searched with the device's own web search (its default
|
||||
// browser or search app answers ACTION_WEB_SEARCH); false where none does,
|
||||
// and the app opens a search engine's address instead.
|
||||
|
|
@ -107,6 +111,12 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (!voiceInput.onActivityResult(requestCode, resultCode, data)) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NOTIFY_REQUEST = 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package app.halkyon.learn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.speech.RecognizerIntent
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* The device's voice input app for halkyon_app's ChannelVoiceInputApp
|
||||
* (Halkyon Meditate's ADR 0038, D18): whichever app handles
|
||||
* ACTION_RECOGNIZE_SPEECH (FUTO Voice Input, Google's) opens over the app,
|
||||
* records and returns text alone, so this app asks for no microphone.
|
||||
* `installed` says whether one is (the manifest's <queries> lets Android 11
|
||||
* on show it); `listen` opens it and answers once it is closed: its first
|
||||
* result, empty where cancelled, an error where it couldn't be opened. The
|
||||
* activity hands its results to [onActivityResult].
|
||||
*/
|
||||
class VoiceInput(private val activity: Activity) {
|
||||
private var pending: MethodChannel.Result? = null
|
||||
|
||||
fun attach(messenger: BinaryMessenger) {
|
||||
MethodChannel(messenger, "app.halkyon/voice_input").setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"installed" -> result.success(installed())
|
||||
"listen" -> {
|
||||
if (pending != null) {
|
||||
result.error("busy", "The voice input app is open already", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||
call.argument<String>("language")?.let { intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, it) }
|
||||
call.argument<String>("prompt")?.let { intent.putExtra(RecognizerIntent.EXTRA_PROMPT, it) }
|
||||
try {
|
||||
pending = result
|
||||
activity.startActivityForResult(intent, REQUEST)
|
||||
} catch (error: ActivityNotFoundException) {
|
||||
pending = null
|
||||
result.error("noApp", error.message, null)
|
||||
} catch (error: SecurityException) {
|
||||
pending = null
|
||||
result.error("refused", error.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun installed(): Boolean {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
val packages = activity.packageManager
|
||||
val found = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packages.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packages.queryIntentActivities(intent, 0)
|
||||
}
|
||||
return found.isNotEmpty()
|
||||
}
|
||||
|
||||
/** Whether [requestCode] was the voice input app's, its text handed on. */
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode != REQUEST) return false
|
||||
val result = pending ?: return true
|
||||
pending = null
|
||||
val text = if (resultCode == Activity.RESULT_OK) {
|
||||
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
result.success(text ?: "")
|
||||
return true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Any number the app's other activity results don't use. */
|
||||
const val REQUEST = 0x766f
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue