First release?

This commit is contained in:
Nehemiah of Zebulun 2023-11-13 11:58:48 -05:00
parent 590975d708
commit 96d0443892
13 changed files with 621 additions and 460 deletions

View File

@ -11,23 +11,13 @@ import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.ExtractedTextRequest
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout import net.mezimmah.wkt9.candidates.Candidates
import android.widget.TextView
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.inputmode.InputManager import net.mezimmah.wkt9.inputmode.InputManager
import net.mezimmah.wkt9.inputmode.InputMode import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Event import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.voice.Whisper import net.mezimmah.wkt9.voice.Whisper
import okio.IOException
import java.io.File
class WKT9: WKT9Interface, InputMethodService() { class WKT9: WKT9Interface, InputMethodService() {
private val tag = "WKT9" private val tag = "WKT9"
@ -38,37 +28,19 @@ class WKT9: WKT9Interface, InputMethodService() {
private var inputView: View? = null private var inputView: View? = null
private var composing: Boolean = false private var composing: Boolean = false
private var lastComposedText: CharSequence = ""
private var cursorPosition: Int = 0 private var cursorPosition: Int = 0
private val keyDownStats = KeyEventStat(0, 0) private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0) private val keyUpStats = KeyEventStat(0, 0)
// Whisper // Whisper
private val whisper: Whisper = Whisper() private lateinit var whisper: Whisper
private var recorder: MediaRecorder? = null private var recorder: MediaRecorder? = null
private var recording: File? = null
private var toast: Toast? = null private lateinit var candidates: Candidates
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) override fun onCandidates(candidates: List<String>, current: Int?) {
private var ioJob: Job? = null this.candidates.load(candidates, current)
private val commitScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var commitJob: Job? = null
override fun onCandidates(
candidates: List<String>,
finishComposing: Boolean,
current: Int?,
timeout: Long?,
start: Int?,
end: Int?,
notifyOnChange: Boolean
) {
if (finishComposing) finishComposingText()
loadCandidates(candidates, current, timeout, start, end, notifyOnChange)
} }
override fun onCancelCompose() { override fun onCancelCompose() {
@ -76,10 +48,24 @@ class WKT9: WKT9Interface, InputMethodService() {
} }
override fun onClearCandidates() { override fun onClearCandidates() {
clearCandidates() candidates.clear()
}
override fun onCompose(text: String, rangeStart: Int, rangeEnd: Int) {
val selectionEnd = rangeStart + text.length
currentInputConnection?.run {
beginBatchEdit()
setComposingRegion(rangeStart, rangeEnd)
commitText(text, 1)
setSelection(rangeStart, selectionEnd)
endBatchEdit()
}
} }
override fun onCreate() { override fun onCreate() {
Log.d(tag, "Starting WKT9")
inputManager = InputManager(this) inputManager = InputManager(this)
super.onCreate() super.onCreate()
@ -87,7 +73,10 @@ class WKT9: WKT9Interface, InputMethodService() {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreateInputView(): View? { override fun onCreateInputView(): View? {
inputView = layoutInflater.inflate(R.layout.suggestions, null) inputView = layoutInflater.inflate(R.layout.suggestions, null).also {
candidates = Candidates(this, inputManager, it)
whisper = Whisper(this, inputManager, it)
}
return inputView return inputView
} }
@ -98,14 +87,16 @@ class WKT9: WKT9Interface, InputMethodService() {
deleteText(beforeCursor, afterCursor) deleteText(beforeCursor, afterCursor)
} }
override fun onFinishComposing() { override fun onFinishComposing(cursorPosition: Int) {
finishComposingText() currentInputConnection?.run {
setSelection(cursorPosition, cursorPosition)
}
} }
override fun onFinishInputView(finishingInput: Boolean) { override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput) super.onFinishInputView(finishingInput)
clearCandidates() onClearCandidates()
} }
override fun onGetTextBeforeCursor(n: Int): CharSequence? { override fun onGetTextBeforeCursor(n: Int): CharSequence? {
@ -243,9 +234,6 @@ class WKT9: WKT9Interface, InputMethodService() {
} }
override fun onStartInput(editorInfo: EditorInfo?, restarting: Boolean) { override fun onStartInput(editorInfo: EditorInfo?, restarting: Boolean) {
ioJob?.cancel() // Cancel possible transcription job
toast?.cancel()
if (editorInfo == null) return if (editorInfo == null) return
inputManager.selectHandler(editorInfo) inputManager.selectHandler(editorInfo)
@ -259,13 +247,7 @@ class WKT9: WKT9Interface, InputMethodService() {
candidatesStart: Int, candidatesStart: Int,
candidatesEnd: Int candidatesEnd: Int
) { ) {
inputManager.handler?.run { inputManager.handler?.onUpdateCursorPosition(newSelEnd)
if (candidatesStart != candidatesEnd) {
onComposeText(lastComposedText, candidatesStart, candidatesEnd)
}
onUpdateCursorPosition(newSelEnd)
}
super.onUpdateSelection( super.onUpdateSelection(
oldSelStart, oldSelStart,
@ -311,37 +293,6 @@ class WKT9: WKT9Interface, InputMethodService() {
} }
} }
private fun clearCandidates() {
val candidatesView = inputView?.findViewById<LinearLayout>(R.id.suggestions) ?: return
candidatesView.removeAllViews()
}
private fun markComposingRegion(start: Int? = null, end: Int? = null) {
this.composing = currentInputConnection?.run {
val composeStart = start ?: cursorPosition
val composeEnd = end ?: cursorPosition
setComposingRegion(composeStart, composeEnd)
} ?: false
}
private fun commitText(text: CharSequence) {
val insertTextStart = cursorPosition
val insertTextEnd = cursorPosition + text.length
currentInputConnection?.commitText(text, 1)
inputManager.handler?.onInsertText(text, insertTextStart, insertTextEnd)
}
private fun composeText(text: CharSequence, cursorPosition: Int = 1) {
if (!composing) return
currentInputConnection?.let {
if (it.setComposingText(text, cursorPosition)) lastComposedText = text
}
}
private fun deleteText(beforeCursor: Int, afterCursor: Int) { private fun deleteText(beforeCursor: Int, afterCursor: Int) {
currentInputConnection?.run { currentInputConnection?.run {
deleteSurroundingText(beforeCursor, afterCursor) deleteSurroundingText(beforeCursor, afterCursor)
@ -360,148 +311,16 @@ class WKT9: WKT9Interface, InputMethodService() {
composing = false composing = false
} }
private fun getCandidateIndex(candidate: View): Int? {
val candidatesView = inputView?.findViewById<LinearLayout>(R.id.suggestions) ?: return null
for (i in 0 until candidatesView.childCount) {
val child: View = candidatesView.getChildAt(i)
if (candidate == child) return i
}
return null
}
private fun loadCandidates(
candidates: List<String>,
current: Int?,
timeout: Long?,
start: Int?,
end: Int?,
notifyOnChange: Boolean = false
) {
val candidatesView = inputView?.findViewById<LinearLayout>(R.id.suggestions) ?: return
clearCandidates()
commitJob?.cancel()
candidates.forEachIndexed { index, candidate ->
val layout = if (index == current) R.layout.current_suggestion else R.layout.suggestion
val candidateView = layoutInflater.inflate(layout, null)
val textView = candidateView.findViewById<TextView>(R.id.suggestion_text)
textView.text = candidate
candidateView.setOnClickListener { view ->
getCandidateIndex(view)?.let { index ->
loadCandidates(candidates, index, timeout, start, end, notifyOnChange)
}
}
candidateView.setOnLongClickListener {
val view = it as LinearLayout
val suggestion = view.findViewById<TextView>(R.id.suggestion_text)
val text = suggestion.text
inputManager.handler?.onLongClickCandidate(text.toString())
true
}
if (index == current) {
if (!composing) markComposingRegion(start, end)
composeText(candidate)
if (notifyOnChange) {
inputManager.handler?.onCandidateSelected(candidate)
}
}
candidatesView.addView(candidateView)
}
if (!isInputViewShown) requestShowSelf(InputMethodManager.SHOW_IMPLICIT)
if (timeout == null) return
commitJob = commitScope.launch {
delay(timeout.toLong())
resetKeyStats()
finishComposingText()
}
}
@Suppress("DEPRECATION")
private fun record() { private fun record() {
// The recorder must be busy... // The recorder must be busy...
if (recorder !== null) return if (recorder != null) return
clearCandidates()
requestShowSelf(InputMethodManager.SHOW_IMPLICIT) requestShowSelf(InputMethodManager.SHOW_IMPLICIT)
// Delete possible existing recording whisper.record()
recording?.delete()
// Toast settings
val text = "Recording now.\nRelease the button to start transcribing."
val duration = Toast.LENGTH_SHORT
// Instantiate recorder and start recording
recorder = MediaRecorder().also {
recording = File.createTempFile("recording.3gp", null, cacheDir)
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
it.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
it.setOutputFile(recording)
try {
it.prepare()
it.start()
toast?.cancel()
toast = Toast.makeText(this, text, duration).apply {
this.show()
}
} catch (e: Exception) {
Log.d(tag, "Failed to start recording", e)
}
}
}
private fun resetKeyStats() {
keyDownStats.keyCode = 0
keyDownStats.repeats = 0
keyUpStats.keyCode = 0
keyUpStats.repeats = 0
} }
private fun transcribe() { private fun transcribe() {
val recorder = this.recorder ?: return whisper.transcribe()
recorder.stop()
recorder.reset()
recorder.release()
this.recorder = null
val text = "Sending recording to speech-to-text server for transcription."
val duration = Toast.LENGTH_SHORT
toast?.cancel()
toast = Toast.makeText(this, text, duration).apply {
this.show()
}
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = whisper.run(recording!!)
commitText(transcription.plus(' '))
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
}
} }
} }

View File

@ -11,21 +11,18 @@ interface WKT9Interface {
fun onCandidates( fun onCandidates(
candidates: List<String>, candidates: List<String>,
finishComposing: Boolean = false, current: Int? = 0
current: Int? = 0,
timeout: Long? = null,
start: Int? = null,
end: Int? = null,
notifyOnChange: Boolean = false
) )
fun onCancelCompose() fun onCancelCompose()
fun onClearCandidates() fun onClearCandidates()
fun onCompose(text: String, rangeStart: Int, rangeEnd: Int)
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean) fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean)
fun onFinishComposing() fun onFinishComposing(cursorPosition: Int)
fun onGetText(): CharSequence? fun onGetText(): CharSequence?

View File

@ -0,0 +1,70 @@
package net.mezimmah.wkt9.candidates
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.TextView
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.inputmode.InputManager
class Candidates(
private val context: WKT9,
private val inputManager: InputManager,
ui: View
) {
private val candidatesView: LinearLayout = ui.findViewById(R.id.suggestions)
fun clear() {
candidatesView.removeAllViews()
}
fun load(candidates: List<String>, current: Int?) {
clear()
candidates.forEachIndexed { index, candidate ->
val layout = if (index == current) R.layout.current_suggestion else R.layout.suggestion
val candidateView = context.layoutInflater.inflate(layout, null)
val textView = candidateView.findViewById<TextView>(R.id.suggestion_text)
textView.text = candidate
candidateView.setOnClickListener { view ->
getIndex(view)?.let { index ->
load(candidates, index)
}
}
candidateView.setOnLongClickListener {
val view = it as LinearLayout
val suggestion = view.findViewById<TextView>(R.id.suggestion_text)
val text = suggestion.text
inputManager.handler?.onLongClickCandidate(text.toString())
true
}
if (index == current) {
inputManager.handler?.onCandidateSelected(index)
}
candidatesView.addView(candidateView)
}
if (!context.isInputViewShown) {
context.requestShowSelf(InputMethodManager.SHOW_IMPLICIT)
}
}
private fun getIndex(candidate: View): Int? {
for (i in 0 until candidatesView.childCount) {
val child: View = candidatesView.getChildAt(i)
if (candidate == child) return i
}
return null
}
}

View File

@ -20,8 +20,8 @@ class IdleInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) { override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) { when (command) {
Command.DIAL -> dial() Command.DIAL -> dial()
Command.CAMERA -> triggerKeyEvent(KeyEvent.KEYCODE_CAMERA, false) Command.CAMERA -> triggerKeyEvent(KeyEvent.KEYCODE_CAMERA)
Command.NUMBER -> triggerOriginalKeyEvent(key, false) Command.NUMBER -> triggerOriginalKeyEvent(key)
else -> Log.d(tag, "Command not implemented: $command") else -> Log.d(tag, "Command not implemented: $command")
} }
} }

View File

@ -1,7 +1,6 @@
package net.mezimmah.wkt9.inputmode package net.mezimmah.wkt9.inputmode
import android.text.InputType import android.text.InputType
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9 import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface import net.mezimmah.wkt9.WKT9Interface
@ -28,9 +27,7 @@ open class InputHandler(
override var cursorPosition: Int = 0 override var cursorPosition: Int = 0
protected set protected set
override fun onCandidateSelected(candidate: String) { override fun onCandidateSelected(index: Int) {}
Log.d(tag, "A candidate has been selected: $candidate")
}
override fun onCommitText() {} override fun onCommitText() {}
@ -38,7 +35,7 @@ open class InputHandler(
override fun onFinish() {} override fun onFinish() {}
override fun onInsertText(text: CharSequence, insertTextStart: Int, insertTextEnd: Int) {} override fun onInsertText(text: CharSequence) {}
override fun onLongClickCandidate(text: String) {} override fun onLongClickCandidate(text: String) {}
@ -76,7 +73,6 @@ open class InputHandler(
wkt9.onCandidates( wkt9.onCandidates(
candidates = candidates, candidates = candidates,
finishComposing = stats.repeats == 0,
current = stats.repeats % candidates.count() current = stats.repeats % candidates.count()
) )
} }
@ -107,17 +103,15 @@ open class InputHandler(
return null return null
} }
protected fun triggerKeyEvent(keyCode: Int, finishComposing: Boolean) { protected fun triggerKeyEvent(keyCode: Int) {
val down = KeyEvent(KeyEvent.ACTION_DOWN, keyCode) val down = KeyEvent(KeyEvent.ACTION_DOWN, keyCode)
val up = KeyEvent(KeyEvent.ACTION_UP, keyCode) val up = KeyEvent(KeyEvent.ACTION_UP, keyCode)
if (finishComposing) wkt9.onFinishComposing()
wkt9.onTriggerKeyEvent(down) wkt9.onTriggerKeyEvent(down)
wkt9.onTriggerKeyEvent(up) wkt9.onTriggerKeyEvent(up)
} }
protected fun triggerOriginalKeyEvent(key: Key, finishComposing: Boolean) { protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode, finishComposing) triggerKeyEvent(key.keyCode)
} }
} }

View File

@ -14,7 +14,7 @@ interface InputHandlerInterface {
val capMode: Int? val capMode: Int?
val cursorPosition: Int? val cursorPosition: Int?
fun onCandidateSelected(candidate: String) fun onCandidateSelected(index: Int)
fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int)
@ -22,7 +22,7 @@ interface InputHandlerInterface {
fun onFinish() fun onFinish()
fun onInsertText(text: CharSequence, insertTextStart: Int, insertTextEnd: Int) fun onInsertText(text: CharSequence)
fun onLongClickCandidate(text: String) fun onLongClickCandidate(text: String)

View File

@ -1,6 +1,7 @@
package net.mezimmah.wkt9.inputmode package net.mezimmah.wkt9.inputmode
import android.text.InputType import android.text.InputType
import android.util.Log
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import net.mezimmah.wkt9.R import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9 import net.mezimmah.wkt9.WKT9
@ -51,7 +52,7 @@ class InputManager(val context: WKT9) {
if (override != null) return switchToHandler(override, editor.initialSelEnd) if (override != null) return switchToHandler(override, editor.initialSelEnd)
val handler = if (numericClasses.contains(typeClass)) { val mode = if (numericClasses.contains(typeClass)) {
InputMode.Number InputMode.Number
} else if (typeClass == InputType.TYPE_CLASS_TEXT) { } else if (typeClass == InputType.TYPE_CLASS_TEXT) {
if (letterVariations.contains(typeVariation) || mode == InputMode.Letter) { if (letterVariations.contains(typeVariation) || mode == InputMode.Letter) {
@ -63,21 +64,28 @@ class InputManager(val context: WKT9) {
InputMode.Idle InputMode.Idle
} }
switchToHandler(handler, editor.initialSelEnd) switchToHandler(mode, editor.initialSelEnd)
} }
fun switchToHandler(inputMode: InputMode, cursorPosition: Int) { fun switchToHandler(inputMode: InputMode, cursorPosition: Int) {
this.handler?.onFinish() val lastHandler = this.handler
this.mode = inputMode val cursor: Int = lastHandler?.cursorPosition ?: cursorPosition
this.handler = when (inputMode) { val newHandler = when (inputMode) {
InputMode.Word -> wordInputHandler InputMode.Word -> wordInputHandler
InputMode.Letter -> letterInputHandler InputMode.Letter -> letterInputHandler
InputMode.Number -> numberInputHandler InputMode.Number -> numberInputHandler
else -> idleInputHandler else -> idleInputHandler
}.apply {
onStart(typeClass, typeVariation, typeFlags)
onUpdateCursorPosition(cursorPosition)
} }
newHandler.apply {
onStart(typeClass, typeVariation, typeFlags)
onUpdateCursorPosition(cursor)
}
lastHandler?.onFinish()
this.mode = inputMode
this.handler = newHandler
} }
private fun selectOverride(packageName: String): InputMode? { private fun selectOverride(packageName: String): InputMode? {

View File

@ -7,11 +7,12 @@ import android.view.KeyEvent
import android.view.textservice.SentenceSuggestionsInfo import android.view.textservice.SentenceSuggestionsInfo
import android.view.textservice.SpellCheckerSession import android.view.textservice.SpellCheckerSession
import android.view.textservice.SuggestionsInfo import android.view.textservice.SuggestionsInfo
import android.view.textservice.TextInfo
import android.view.textservice.TextServicesManager import android.view.textservice.TextServicesManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9 import net.mezimmah.wkt9.WKT9
@ -25,20 +26,25 @@ import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout import net.mezimmah.wkt9.keypad.KeyLayout
import java.util.Locale import java.util.Locale
import kotlin.text.StringBuilder
class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSession.SpellCheckerSessionListener, InputHandler(wkt9, context) { class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSession.SpellCheckerSessionListener, InputHandler(wkt9, context) {
private var db: AppDatabase private var db: AppDatabase
private var wordDao: WordDao private var wordDao: WordDao
private var locale: Locale? = null private var locale: Locale? = null
private var spellCheckerSession: SpellCheckerSession? = null private var spellCheckerSession: SpellCheckerSession? = null
private val candidates = mutableListOf<String>()
private var candidateIndex: Int? = null
private var composeRangeStart: Int? = null
private var composeRangeEnd: Int? = null
private var sentenceStart: Boolean = false private var sentenceStart: Boolean = false
private var wordStart: Boolean = false private var wordStart: Boolean = false
private var selectionStart: Int = 0
private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val timeoutScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var timeoutJob: Job? = null
private val composing = StringBuilder()
private val content = StringBuilder() private val content = StringBuilder()
init { init {
@ -66,39 +72,42 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
updateIcon() updateIcon()
} }
override fun onCandidateSelected(candidate: String) { override fun finalizeWordOrSentence(stats: KeyEventStat) {
wkt9.onFinishComposing() if (stats.repeats == 0) {
timeoutJob?.cancel()
clearCandidates(true)
storeLastWord()
} }
override fun onCommitText() { candidates.addAll(listOf(" ", ". ", "? ", "! ", ", ", ": ", "; "))
composing.clear() wkt9.onCandidates(candidates, stats.repeats % candidates.count())
wkt9.onClearCandidates()
val info = getCursorPositionInfo(content)
if (info.startSentence || info.startWord) return
val last = content.split("\\s".toRegex()).last()
if (last.length > 2) getSuggestions(last)
} }
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) { override fun onCandidateSelected(index: Int) {
val info = getCursorPositionInfo(text) val candidate = candidates[index]
val lastWord = content.split("\\s+".toRegex()).last() val rangeStart = composeRangeStart ?: cursorPosition
val rangeEnd = composeRangeEnd ?: cursorPosition
if (lastWord.isNotEmpty() && (info.startSentence || info.startWord)) { content.replace(rangeStart, rangeEnd, candidate)
storeWord(lastWord)
}
content.replace(composingTextStart, composingTextEnd, text.toString()) candidateIndex = index
composing.replace(0, composing.length, text.toString()) composeRangeStart = rangeStart
composeRangeEnd = rangeStart + candidate.length
wkt9.onCompose(candidate, rangeStart, rangeEnd)
} }
override fun onFinish() { override fun onFinish() {
super.onFinish() timeoutJob?.cancel()
wkt9.onFinishComposing() clearCandidates(true)
cursorPosition = 0
sentenceStart = false
wordStart = false
content.clear()
} }
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) { override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
@ -106,32 +115,15 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
} }
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) { override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
val candidates = mutableListOf<String>() TODO("Not yet implemented")
results?.map {
val suggestions = it.getSuggestionsInfoAt(0)
for (index in 0 until suggestions.suggestionsCount) {
val suggestion = suggestions.getSuggestionAt(index)
candidates.add(suggestion)
}
} }
if (candidates.isEmpty()) return override fun onInsertText(text: CharSequence) {
clearCandidates(true)
wkt9.onCandidates( content.replace(cursorPosition, cursorPosition, text.toString())
candidates = candidates, wkt9.onCompose(text.toString(), cursorPosition, cursorPosition)
current = null, wkt9.onFinishComposing(cursorPosition + text.length)
start = selectionStart,
end = cursorPosition,
notifyOnChange = true
)
}
// This is for the text that should be committed without 'handling'.
override fun onInsertText(text: CharSequence, insertTextStart: Int, insertTextEnd: Int) {
content.replace(insertTextStart, insertTextEnd, text.toString())
} }
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) { override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
@ -141,7 +133,7 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
Command.DELETE -> delete() Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key) Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor() Command.MOVE_CURSOR -> moveCursor()
Command.NUMBER -> triggerOriginalKeyEvent(key, true) Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> wkt9.onRecord(true) Command.RECORD -> wkt9.onRecord(true)
Command.SPACE -> finalizeWordOrSentence(stats) Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.onTranscribe() Command.TRANSCRIBE -> wkt9.onTranscribe()
@ -149,6 +141,21 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
} }
} }
override fun onUpdateCursorPosition(cursorPosition: Int) {
super.onUpdateCursorPosition(cursorPosition)
try {
val info = getCursorPositionInfo(content.substring(0, cursorPosition))
sentenceStart = info.startSentence
wordStart = info.startWord
} catch (e: Exception) {
Log.d(tag, "Cursor position out of range.\nContent: $content\nCursor position: $cursorPosition", e)
}
updateIcon()
}
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) { override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {
capMode = getDefaultCapMode(typeFlags) capMode = getDefaultCapMode(typeFlags)
@ -158,72 +165,78 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
} ?: content.clear() } ?: content.clear()
} }
override fun onUpdateCursorPosition(cursorPosition: Int) { private fun composeCharacter(key: Key, stats: KeyEventStat) {
super.onUpdateCursorPosition(cursorPosition) val layout = KeyLayout.en_US[key] ?: return
val capitalize = Capitalize(capMode)
if (cursorPosition > content.length) return if (stats.repeats == 0) clearCandidates(true)
var info: CursorPositionInfo layout.forEach {
candidates.add(capitalize.character(it, sentenceStart, wordStart))
if (cursorPosition == 0) {
info = CursorPositionInfo(
startSentence = true,
startWord = true
)
} else if (composing.isNotEmpty()) {
info = getCursorPositionInfo(composing)
if (!info.startSentence && !info.startWord) {
info = getCursorPositionInfo(content.substring(0, cursorPosition - composing.length))
}
} else {
info = getCursorPositionInfo(content.substring(0, cursorPosition))
} }
sentenceStart = info.startSentence wkt9.onCandidates(
wordStart = info.startWord candidates = candidates,
current = stats.repeats % candidates.count()
)
updateIcon() timeoutJob?.cancel()
timeoutJob = timeoutScope.launch {
delay(400)
clearCandidates(true)
// getSuggestions()
}
} }
private fun getSuggestions(text: String){ private fun clearCandidates(finishComposing: Boolean) {
val words = arrayOf( if (finishComposing && candidateIndex != null) {
TextInfo( wkt9.onFinishComposing(cursorPosition)
text.plus("#"), // Add hash to string to get magic performance }
0,
text.length + 1, // We added the hash, remember
0,
0
)
)
selectionStart = cursorPosition - text.length candidateIndex = null
composeRangeStart = null
composeRangeEnd = null
spellCheckerSession?.getSentenceSuggestions(words, 15) candidates.clear()
wkt9.onClearCandidates()
}
private fun delete() {
if (candidateIndex != null) undoCandidate()
else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, true)
}
}
private fun getLastWord(): String {
val beforeCursor = content.substring(0, cursorPosition)
val words = beforeCursor.split("\\s+".toRegex())
return words.last()
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Word)
else wkt9.onSwitchInputHandler(InputMode.Number)
} }
private fun moveCursor() { private fun moveCursor() {
if (composing.isNotEmpty()) wkt9.onFinishComposing() timeoutJob?.cancel()
clearCandidates(true)
} }
private fun storeWord(text: String) { private fun storeLastWord() {
val lastWord = getLastWord()
// We're not storing single char words... // We're not storing single char words...
if (text.length < 2) return if (lastWord.length < 2) return
val words = text.trim().split("\\s+".toRegex())
if (words.count() > 1) {
words.forEach { storeWord(it) }
return
}
try { try {
val codeword = keypad.getCodeForWord(text) val codeword = keypad.getCodeForWord(lastWord)
val word = Word( val word = Word(
word = text, word = lastWord,
code = codeword, code = codeword,
length = text.length, length = lastWord.length,
weight = 1, weight = 1,
locale = "en_US" locale = "en_US"
) )
@ -236,38 +249,17 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
} }
} }
private fun composeCharacter(key: Key, stats: KeyEventStat) { private fun undoCandidate() {
val layout = KeyLayout.en_US[key] ?: return val rangeStart = composeRangeStart ?: cursorPosition
val capitalize = Capitalize(capMode) val rangeEnd = composeRangeEnd ?: cursorPosition
val candidates = mutableListOf<String>()
if (stats.repeats == 0 && composing.isNotEmpty()) wkt9.onFinishComposing() // Remove text codeword produced from editor
wkt9.onCompose("", rangeStart, rangeEnd)
layout.forEach { // Remove text codeword produced from content
candidates.add(capitalize.character(it, sentenceStart, wordStart)) content.deleteRange(rangeStart, rangeEnd)
}
wkt9.onCandidates( clearCandidates(false)
candidates = candidates,
current = stats.repeats % candidates.count(),
timeout = 400L
)
}
private fun delete() {
if (composing.isNotEmpty()) {
wkt9.onCancelCompose()
content.delete(cursorPosition - composing.length, cursorPosition)
composing.clear()
} else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, true)
}
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Word)
else wkt9.onSwitchInputHandler(InputMode.Number)
} }
private fun updateIcon() { private fun updateIcon() {
@ -281,3 +273,42 @@ class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSessio
wkt9.onUpdateStatusIcon(icon) wkt9.onUpdateStatusIcon(icon)
} }
} }
//
// override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
// results?.map {
// val suggestions = it.getSuggestionsInfoAt(0)
//
// for (index in 0 until suggestions.suggestionsCount) {
// val suggestion = suggestions.getSuggestionAt(index)
//
// candidates.add(suggestion)
// }
// }
//
// if (candidates.isEmpty()) return
//
// candidateSource = CandidateSource.Dictionary
//
// wkt9.onCandidates(
// candidates = candidates,
// current = null
// )
// }
// private fun getSuggestions() {
// val lastWord = getLastWord()
//
// if (lastWord.length < 3) return
//
// val words = arrayOf(
// TextInfo(
// lastWord.plus("#"), // Add hash to string to get magic performance
// 0,
// lastWord.length + 1, // We added the hash, remember
// 0,
// 0
// )
// )
//
// spellCheckerSession?.getSentenceSuggestions(words, 15)
// }

View File

@ -23,7 +23,10 @@ import net.mezimmah.wkt9.t9.T9
class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9, context) { class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9, context) {
private val content = StringBuilder() private val content = StringBuilder()
private val codeword = StringBuilder() private val codeword = StringBuilder()
private val composing = StringBuilder() private val candidates = mutableListOf<String>()
private var candidateIndex: Int? = null
private var composeRangeStart: Int? = null
private var composeRangeEnd: Int? = null
private var db: AppDatabase private var db: AppDatabase
private var wordDao: WordDao private var wordDao: WordDao
@ -63,24 +66,45 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
if (codeword.isNotEmpty()) handleCodewordChange(codeword) if (codeword.isNotEmpty()) handleCodewordChange(codeword)
} }
override fun onCommitText() { override fun finalizeWordOrSentence(stats: KeyEventStat) {
if (codeword.isNotEmpty()) increaseWordWeight(composing.toString()) if (codeword.isNotEmpty()) commit()
clearCodeword() candidates.addAll(listOf(" ", ". ", "? ", "! ", ", ", ": ", "; "))
wkt9.onCandidates(candidates, stats.repeats % candidates.count())
} }
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) { override fun onCandidateSelected(index: Int) {
content.replace(composingTextStart, composingTextEnd, text.toString()) val candidate = candidates[index]
composing.replace(0, composing.length, text.toString()) val rangeStart = composeRangeStart ?: cursorPosition
val rangeEnd = composeRangeEnd ?: cursorPosition
content.replace(rangeStart, rangeEnd, candidate)
candidateIndex = index
composeRangeStart = rangeStart
composeRangeEnd = rangeStart + candidate.length
wkt9.onCompose(candidate, rangeStart, rangeEnd)
} }
// This is for the text that should be committed without 'handling'. override fun onInsertText(text: CharSequence) {
override fun onInsertText(text: CharSequence, insertTextStart: Int, insertTextEnd: Int) { commit()
content.replace(insertTextStart, insertTextEnd, text.toString())
content.replace(cursorPosition, cursorPosition, text.toString())
wkt9.onCompose(text.toString(), cursorPosition, cursorPosition)
wkt9.onFinishComposing(cursorPosition + text.length)
} }
override fun onFinish() { override fun onFinish() {
if (composing.isNotEmpty()) wkt9.onFinishComposing() queryJob?.cancel()
commit()
cursorPosition = 0
sentenceStart = false
wordStart = false
content.clear()
} }
override fun onLongClickCandidate(text: String) { override fun onLongClickCandidate(text: String) {
@ -98,8 +122,8 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
Command.DELETE -> delete() Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key) Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor() Command.MOVE_CURSOR -> moveCursor()
Command.NUMBER -> triggerOriginalKeyEvent(key, true) Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> wkt9.onRecord(true) Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats) Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.onTranscribe() Command.TRANSCRIBE -> wkt9.onTranscribe()
else -> Log.d(tag, "Command not implemented: $command") else -> Log.d(tag, "Command not implemented: $command")
@ -107,10 +131,6 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
} }
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) { override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {
codeword.clear()
content.clear()
composing.clear()
capMode = getDefaultCapMode(typeFlags) capMode = getDefaultCapMode(typeFlags)
// Get current editor content on start // Get current editor content on start
@ -122,27 +142,14 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
override fun onUpdateCursorPosition(cursorPosition: Int) { override fun onUpdateCursorPosition(cursorPosition: Int) {
super.onUpdateCursorPosition(cursorPosition) super.onUpdateCursorPosition(cursorPosition)
if (cursorPosition > content.length) return try {
val info = getCursorPositionInfo(content.substring(0, cursorPosition))
var info: CursorPositionInfo
if (cursorPosition == 0) {
info = CursorPositionInfo(
startSentence = true,
startWord = true
)
} else if (composing.isNotEmpty()) {
info = getCursorPositionInfo(composing)
if (!info.startSentence && !info.startWord) {
info = getCursorPositionInfo(content.substring(0, cursorPosition - composing.length))
}
} else {
info = getCursorPositionInfo(content.substring(0, cursorPosition))
}
sentenceStart = info.startSentence sentenceStart = info.startSentence
wordStart = info.startWord wordStart = info.startWord
} catch (e: Exception) {
Log.d(tag, "Cursor position out of range", e)
}
updateIcon() updateIcon()
} }
@ -151,44 +158,55 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
// Don't build fruitless codeword // Don't build fruitless codeword
if (staleCodeword) return if (staleCodeword) return
val code = KeyLayout.numeric[key] if (codeword.isEmpty() && candidateIndex != null) clearCandidates(true)
/** val code = KeyLayout.numeric[key]
* This happens when some other method than buildCodeword composed text. With, for example,
* finalizeWordOrSentence.
*/
if (codeword.isEmpty()) wkt9.onFinishComposing()
codeword.append(code) codeword.append(code)
handleCodewordChange(codeword) handleCodewordChange(codeword)
} }
private fun clearCodeword() { private fun clearCandidates(finishComposing: Boolean) {
codeword.clear() candidateIndex = null
composing.clear() composeRangeStart = null
composeRangeEnd = null
candidates.clear()
wkt9.onClearCandidates()
if (finishComposing) wkt9.onFinishComposing(cursorPosition)
}
private fun clearCodeword(increaseWordWeight: Boolean) {
staleCodeword = false staleCodeword = false
codeword.clear()
if (!increaseWordWeight) return
candidateIndex?.let {
val candidate = candidates[it]
if (candidate.isNotEmpty()) increaseWordWeight(candidate)
}
}
private fun commit() {
if (codeword.isNotEmpty()) clearCodeword(true)
if (candidates.isNotEmpty()) clearCandidates(true)
} }
private fun delete() { private fun delete() {
staleCodeword = false staleCodeword = false
if (codeword.length > 1) { if (codeword.length > 1) reduceCodeword()
codeword.deleteAt(codeword.length - 1) else if (codeword.isNotEmpty()) undoCodeword()
else if (candidateIndex != null) undoCandidate()
handleCodewordChange(codeword) else if (content.isNotEmpty()) {
} else if (codeword.isNotEmpty()) {
clearCodeword()
wkt9.onCancelCompose()
content.deleteAt(content.length - 1) content.deleteAt(content.length - 1)
} else if (composing.isNotEmpty()) { wkt9.onDeleteText(1, 0, false)
wkt9.onCancelCompose()
content.delete(cursorPosition - composing.length, cursorPosition)
composing.clear()
} else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, true)
} }
} }
@ -196,14 +214,10 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
queryJob?.cancel() queryJob?.cancel()
queryJob = queryScope.launch { queryJob = queryScope.launch {
val candidates = queryT9Candidates(codeword, 25) queryT9Candidates(codeword, 25)
if (candidates.isEmpty()) { if (candidates.isEmpty()) staleCodeword = true
staleCodeword = true else {
queryJob?.cancel()
wkt9.onClearCandidates()
} else {
wkt9.onCandidates( wkt9.onCandidates(
candidates = candidates, candidates = candidates,
current = 0 current = 0
@ -224,10 +238,18 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
} }
private fun moveCursor() { private fun moveCursor() {
if (composing.isEmpty()) return commit()
}
wkt9.onFinishComposing() private fun record() {
wkt9.onClearCandidates() commit()
wkt9.onRecord(true)
}
private fun reduceCodeword() {
codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword)
} }
private fun updateIcon() { private fun updateIcon() {
@ -241,15 +263,32 @@ class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9,
wkt9.onUpdateStatusIcon(icon) wkt9.onUpdateStatusIcon(icon)
} }
private suspend fun queryT9Candidates(codeWord: StringBuilder, limit: Int = 10): List<String> { private fun undoCodeword() {
undoCandidate()
clearCodeword(false)
}
private fun undoCandidate() {
val rangeStart = composeRangeStart ?: cursorPosition
val rangeEnd = composeRangeEnd ?: cursorPosition
// Remove text codeword produced from editor
wkt9.onCompose("", rangeStart, rangeEnd)
// Remove text codeword produced from content
content.deleteRange(rangeStart, rangeEnd)
clearCandidates(false)
}
private suspend fun queryT9Candidates(codeWord: StringBuilder, limit: Int = 10) {
val results = wordDao.findCandidates(codeWord.toString(), limit) val results = wordDao.findCandidates(codeWord.toString(), limit)
val capitalize = Capitalize(capMode) val capitalize = Capitalize(capMode)
val candidates = mutableListOf<String>()
candidates.clear()
results.forEach { result -> results.forEach { result ->
candidates.add(capitalize.word(result.word, sentenceStart)) candidates.add(capitalize.word(result.word, sentenceStart))
} }
return candidates
} }
} }

View File

@ -1,5 +1,18 @@
package net.mezimmah.wkt9.voice package net.mezimmah.wkt9.voice
import android.media.MediaRecorder
import android.util.Log
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.inputmode.InputManager
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -9,15 +22,113 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class Whisper { class Whisper(
private val context: WKT9,
private val inputManager: InputManager,
private val ui: View
) {
private val tag = "WKT9"
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var ioJob: Job? = null
private var recorder: MediaRecorder? = null
private var recording: File? = null
private val client: OkHttpClient = OkHttpClient.Builder() private val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS) .connectTimeout(2, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS) .readTimeout(25, TimeUnit.SECONDS)
.callTimeout(32, TimeUnit.SECONDS) .callTimeout(32, TimeUnit.SECONDS)
.build() .build()
fun transcribe() {
stopRecording()
fun run(recording: File): String { val recording = this.recording ?: return
showTranscribing()
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = run(recording)
mainScope.launch {
showCandidates()
}
inputManager.handler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
}
}
@Suppress("DEPRECATION")
fun record() {
if (recorder != null) stopRecording()
showMessage()
recording = File.createTempFile("recording.3gp", null, context.cacheDir)
recorder = MediaRecorder().also {
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
it.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
it.setOutputFile(recording)
try {
it.prepare()
it.start()
} catch (e: Exception) {
Log.d(tag, "Failed to start recording", e)
}
}
}
private fun showCandidates() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
candidatesView.visibility = View.VISIBLE
loadingView.visibility = View.GONE
messageView.visibility = View.GONE
}
private fun showMessage() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
candidatesView.visibility = View.GONE
loadingView.visibility = View.GONE
messageView.visibility = View.VISIBLE
}
private fun showTranscribing() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
candidatesView.visibility = View.GONE
loadingView.visibility = View.VISIBLE
messageView.visibility = View.GONE
}
private fun stopRecording() {
recorder?.run {
stop()
reset()
release()
}
recorder = null
}
private fun run(recording: File): String {
val mediaType = "audio/3gpp".toMediaType() val mediaType = "audio/3gpp".toMediaType()
val requestBody = MultipartBody.Builder() val requestBody = MultipartBody.Builder()

View File

@ -0,0 +1,5 @@
<vector android:height="48dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="m439.5,236c0,-11.3 -9.1,-20.4 -20.4,-20.4s-20.4,9.1 -20.4,20.4c0,70 -64,126.9 -142.7,126.9 -78.7,0 -142.7,-56.9 -142.7,-126.9 0,-11.3 -9.1,-20.4 -20.4,-20.4s-20.4,9.1 -20.4,20.4c0,86.2 71.5,157.4 163.1,166.7v57.5h-23.6c-11.3,0 -20.4,9.1 -20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h88c11.3,0 20.4,-9.1 20.4,-20.4 0,-11.3 -9.1,-20.4 -20.4,-20.4h-23.6v-57.5c91.6,-9.3 163.1,-80.5 163.1,-166.7z"/>
<path android:fillColor="#FFFFFF" android:pathData="m256,323.5c51,0 92.3,-41.3 92.3,-92.3v-127.9c0,-51 -41.3,-92.3 -92.3,-92.3s-92.3,41.3 -92.3,92.3v127.9c0,51 41.3,92.3 92.3,92.3zM203.7,103.3c0,-28.8 23.5,-52.3 52.3,-52.3s52.3,23.5 52.3,52.3v127.9c0,28.8 -23.5,52.3 -52.3,52.3s-52.3,-23.5 -52.3,-52.3v-127.9z"/>
</vector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom"
android:background="@color/black"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/suggestions"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="40dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing, please wait..." />
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<HorizontalScrollView <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -8,6 +8,12 @@
android:theme="@style/Theme.AppCompat.DayNight" android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom" android:gravity="bottom"
android:background="@color/black" android:background="@color/black"
android:orientation="vertical">
<HorizontalScrollView
android:id="@+id/suggestion_container"
android:layout_width="match_parent"
android:layout_height="44dp"
android:orientation="horizontal"> android:orientation="horizontal">
<LinearLayout <LinearLayout
@ -16,4 +22,56 @@
android:layout_height="44dp" android:layout_height="44dp"
android:orientation="horizontal" /> android:orientation="horizontal" />
</HorizontalScrollView> </HorizontalScrollView>
<LinearLayout
android:id="@+id/loading_container"
android:layout_width="match_parent"
android:layout_height="44dp"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="2dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing, please wait..." />
</LinearLayout>
<LinearLayout
android:id="@+id/message_container"
android:layout_width="match_parent"
android:layout_height="44dp"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="2dp"
android:src="@drawable/mic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing, please wait..." />
</LinearLayout>
</LinearLayout>