Recently Written · git

assistkey

Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords

git clone https://github.com/equwal/assistkey

Log | Files | Refs


app/src/main/java/dev/equwal/assistkey/voice/DictationActivity.kt (7788 bytes)

1 package dev.equwal.assistkey.voice
2 
3 import android.app.Activity
4 import android.content.Intent
5 import android.graphics.Color
6 import android.os.Bundle
7 import android.os.Handler
8 import android.os.Looper
9 import android.speech.RecognitionListener
10 import android.speech.RecognizerIntent
11 import android.speech.SpeechRecognizer
12 import android.util.Log
13 import android.util.TypedValue
14 import android.view.Gravity
15 import android.view.WindowManager
16 import android.widget.FrameLayout
17 import android.widget.TextView
18 import dev.equwal.assistkey.route.ServiceHolder
19 
20 /**
21  * Listens, and hands the words to [TextInsert].
22  *
23  * The activity exists so that this app is in front while the microphone is in
24  * use; Android allows nothing else. It is see-through, and its window cannot
25  * take focus or touches, so the text field behind it keeps the cursor and the
26  * keyboard. The only thing it draws is a one-line label.
27  */
28 class DictationActivity : Activity() {
29 
30     private companion object {
31         private const val SCREEN = 1
32         /** Time for the window behind to be the front window again. */
33         const val INSERT_DELAY_MS = 300L
34     }
35 
36     private var recognizer: SpeechRecognizer? = null
37     private lateinit var label: TextView
38 
39     override fun onCreate(savedInstanceState: Bundle?) {
40         super.onCreate(savedInstanceState)
41         window.addFlags(
42             WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
43                 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
44                 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
45         )
46         label = TextView(this).apply {
47             text = "Listening..."
48             setTextColor(Color.WHITE)
49             setBackgroundColor(Color.BLACK)
50             setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
51             val p = (16 * resources.displayMetrics.density).toInt()
52             setPadding(p, p * 3, p, p)
53         }
54         setContentView(
55             FrameLayout(this).apply {
56                 addView(
57                     label,
58                     FrameLayout.LayoutParams(
59                         FrameLayout.LayoutParams.MATCH_PARENT,
60                         FrameLayout.LayoutParams.WRAP_CONTENT,
61                         Gravity.TOP
62                     )
63                 )
64             }
65         )
66     }
67 
68     override fun onResume() {
69         super.onResume()
70         if (recognizer != null || onScreen) return
71         if (Dictation.usesScreen(this, Dictation.engine(this)) && listenOnScreen()) return
72         listen()
73     }
74 
75     /** True while the speech app listens on a screen of its own. */
76     private var onScreen = false
77 
78     /**
79      * The other way to listen: the speech screen of the speech app, which
80      * gives the words back as a result. See [Dictation.fallBackToScreen].
81      */
82     private fun listenOnScreen(): Boolean {
83         val engine = Dictation.engine(this) ?: return false
84         val ask = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).setPackage(engine.packageName)
85             .putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
86         Dictation.language(this).takeIf { it.isNotEmpty() }?.let { ask.putExtra(RecognizerIntent.EXTRA_LANGUAGE, it) }
87         if (ask.resolveActivity(packageManager) == null) return false
88         return runCatching { startActivityForResult(ask, SCREEN) }.isSuccess.also { onScreen = it }
89     }
90 
91     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
92         super.onActivityResult(requestCode, resultCode, data)
93         if (requestCode != SCREEN) return
94         onScreen = false
95         done(data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.firstOrNull())
96     }
97 
98     override fun onDestroy() {
99         close()
100         super.onDestroy()
101     }
102 
103     private fun listen() {
104         val r = runCatching {
105             Dictation.engine(this)?.let { SpeechRecognizer.createSpeechRecognizer(this, it) }
106                 ?: SpeechRecognizer.createSpeechRecognizer(this)
107         }.getOrNull()
108         if (r == null) {
109             Dictation.toast(this, "The speech app did not start")
110             return done(null)
111         }
112         recognizer = r
113         Dictation.stop = { r.stopListening() }
114         r.setRecognitionListener(listener)
115         r.startListening(
116             Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
117                 putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
118                 putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
119                 putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, packageName)
120                 Dictation.language(this@DictationActivity).takeIf { it.isNotEmpty() }
121                     ?.let { putExtra(RecognizerIntent.EXTRA_LANGUAGE, it) }
122             }
123         )
124     }
125 
126     private val listener = object : RecognitionListener {
127         override fun onResults(results: Bundle?) =
128             done(results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull())
129 
130         override fun onError(error: Int) {
131             Log.i("AssistKey", "speech recognition error " + error)
132             val engine = Dictation.engine(this@DictationActivity)
133             if (Dictation.fallBackToScreen(error, screenAvailable = engine != null)) {
134                 close()
135                 if (listenOnScreen()) {
136                     Dictation.rememberScreen(this@DictationActivity, engine)
137                     return
138                 }
139             }
140             Dictation.toast(
141                 this@DictationActivity,
142                 when (error) {
143                     SpeechRecognizer.ERROR_NO_MATCH, SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Heard nothing"
144                     SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "The microphone is not allowed"
145                     SpeechRecognizer.ERROR_NETWORK, SpeechRecognizer.ERROR_NETWORK_TIMEOUT ->
146                         "The speech app has no connection"
147                     else -> "Speech failed (" + error + ")"
148                 }
149             )
150             done(null)
151         }
152 
153         override fun onPartialResults(partial: Bundle?) {
154             partial?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull()
155                 ?.takeIf { it.isNotBlank() }?.let { label.text = it }
156         }
157 
158         override fun onEndOfSpeech() { label.text = "Working..." }
159         override fun onReadyForSpeech(params: Bundle?) = Unit
160         override fun onBeginningOfSpeech() = Unit
161         override fun onRmsChanged(rmsdB: Float) = Unit
162         override fun onBufferReceived(buffer: ByteArray?) = Unit
163         override fun onEvent(eventType: Int, params: Bundle?) = Unit
164     }
165 
166     private fun close() {
167         Dictation.stop = null
168         runCatching { recognizer?.destroy() }
169         recognizer = null
170     }
171 
172     /** Leaves first, then writes: the field must be the front window again. */
173     private fun done(text: String?) {
174         Log.i("AssistKey", "voice typing: " + if (text.isNullOrBlank()) "no text" else text.length.toString() + " characters")
175         close()
176         finish()
177         @Suppress("DEPRECATION")
178         overridePendingTransition(0, 0)
179         if (text.isNullOrBlank()) return
180         val app = applicationContext
181         Handler(Looper.getMainLooper()).postDelayed({
182             val svc = ServiceHolder.service ?: return@postDelayed
183             when (TextInsert.insert(svc, text)) {
184                 TextInsert.Result.DONE -> Unit
185                 TextInsert.Result.NO_FIELD -> Dictation.toast(app, "No text field has the cursor")
186                 TextInsert.Result.PASSWORD -> Dictation.toast(app, "Not into a password field")
187                 TextInsert.Result.REFUSED -> Dictation.toast(app, "That field did not accept the text")
188             }
189         }, INSERT_DELAY_MS)
190     }
191 }