Making progress

This commit is contained in:
Nehemiah of Zebulun 2023-12-01 16:30:59 -05:00
parent 96d0443892
commit a7e17eda9d
57 changed files with 1041 additions and 1785 deletions

View File

@ -22,7 +22,7 @@
android:theme="@style/Theme.WKT9">
<service
android:name=".WKT9"
android:name=".WKT9IME"
android:label="@string/app_name"
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">

View File

@ -2,39 +2,36 @@ package net.mezimmah.wkt9
import android.content.Intent
import android.view.KeyEvent
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
interface WKT9Interface {
interface IME {
fun onTriggerKeyEvent(event: KeyEvent)
fun onStartIntent(intent: Intent)
fun onCandidates(
candidates: List<String>,
candidates: ArrayList<CharSequence>,
current: Int? = 0
)
fun onCancelCompose()
fun onWords(words: List<Word>)
fun onClearCandidates()
fun onNextWord()
fun onCompose(text: String, rangeStart: Int, rangeEnd: Int)
fun onPreviousWord()
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean)
fun onWordSelected(word: Word)
fun onFinishComposing(cursorPosition: Int)
fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0)
fun onGetText(): CharSequence?
fun onCompose(text: CharSequence)
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false)
fun onGetTextBeforeCursor(n: Int): CharSequence?
fun onReplaceText(text: String)
fun onSwitchInputHandler(inputMode: InputMode)
fun onRecord(finishComposing: Boolean)
fun onTranscribe()
fun onUpdateStatusIcon(icon: Int?)
}

View File

@ -3,113 +3,110 @@ package net.mezimmah.wkt9
import android.annotation.SuppressLint
import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.media.MediaRecorder
import android.provider.Settings
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import net.mezimmah.wkt9.candidates.Candidates
import net.mezimmah.wkt9.inputmode.InputManager
import android.view.inputmethod.InputMethodSubtype
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputhandler.IdleInputHandler
import net.mezimmah.wkt9.inputhandler.InputHandler
import net.mezimmah.wkt9.inputhandler.LetterInputHandler
import net.mezimmah.wkt9.inputhandler.NumberInputHandler
import net.mezimmah.wkt9.inputhandler.WordInputHandler
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.voice.Whisper
import net.mezimmah.wkt9.layout.Words
import net.mezimmah.wkt9.t9.T9
import java.util.Locale
class WKT9: WKT9Interface, InputMethodService() {
class WKT9IME: IME, InputMethodService() {
private val tag = "WKT9"
private val longPressTimeout = 400L
private lateinit var inputManager: InputManager
private val inputModeManager = InputModeManager(this)
private var inputView: View? = null
private lateinit var locale: Locale
private var inputHandler: InputHandler? = null
private var composing: Boolean = false
private var cursorPosition: Int = 0
private var wordsView: Words? = null
private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0)
// Whisper
private lateinit var whisper: Whisper
private var recorder: MediaRecorder? = null
private var composing: Boolean = false
private var selectionStart: Int = 0
private var selectionEnd: Int = 0
private lateinit var candidates: Candidates
override fun onCandidates(candidates: List<String>, current: Int?) {
this.candidates.load(candidates, current)
override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) {
// this.candidates?.load(candidates, current)
}
override fun onCancelCompose() {
cancelCompose()
}
override fun onClearCandidates() {
candidates.clear()
}
override fun onCompose(text: String, rangeStart: Int, rangeEnd: Int) {
val selectionEnd = rangeStart + text.length
override fun onCommit(text: CharSequence, beforeCursor: Int, afterCursor: Int) {
currentInputConnection?.run {
beginBatchEdit()
setComposingRegion(rangeStart, rangeEnd)
commitText(text, 1)
setSelection(rangeStart, selectionEnd)
setComposingRegion(selectionEnd - beforeCursor, selectionEnd + afterCursor)
setComposingText(text, 1)
finishComposingText()
endBatchEdit()
}
}
override fun onCompose(text: CharSequence) {
currentInputConnection?.run {
if (!composing) setComposingRegion(selectionStart, selectionEnd)
setComposingText(text, 1)
}
}
@SuppressLint("InflateParams")
override fun onCreate() {
Log.d(tag, "Starting WKT9")
inputManager = InputManager(this)
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val languageTag = inputMethodManager.currentInputMethodSubtype?.languageTag ?: "en-US"
locale = Locale.forLanguageTag(languageTag)
initializeDictionary(locale)
super.onCreate()
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
inputView = layoutInflater.inflate(R.layout.suggestions, null).also {
candidates = Candidates(this, inputManager, it)
whisper = Whisper(this, inputManager, it)
}
wordsView = layoutInflater.inflate(R.layout.words, null) as Words
return inputView
return wordsView
}
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
super.onCurrentInputMethodSubtypeChanged(newSubtype)
newSubtype?.let {
locale = Locale.forLanguageTag(it.languageTag)
initializeDictionary(locale)
inputHandler?.onSwitchLocale(locale)
}
}
override fun onDeleteText(beforeCursor: Int, afterCursor: Int, finishComposing: Boolean) {
if (finishComposing) finishComposingText()
deleteText(beforeCursor, afterCursor)
}
override fun onFinishComposing(cursorPosition: Int) {
currentInputConnection?.run {
setSelection(cursorPosition, cursorPosition)
}
}
override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput)
onClearCandidates()
}
override fun onGetTextBeforeCursor(n: Int): CharSequence? {
return this.currentInputConnection?.getTextBeforeCursor(n, 0)
}
override fun onGetText(): CharSequence? {
val request = ExtractedTextRequest()
val text = currentInputConnection.getExtractedText(request, 0)
return text?.text
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyDownStats.keyCode != keyCode) {
keyDownStats.keyCode = keyCode
@ -124,16 +121,16 @@ class WKT9: WKT9Interface, InputMethodService() {
val hasLongDownMapping = key.mappings.hasLongDownMapping(InputMode.Word)
val mappings = key.mappings.match(
event = if (event.repeatCount > 0) Event.keyDownRepeat else Event.keyDown,
inputMode = inputManager.mode,
inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputManager.handler?.onRunCommand(mapping.command, key, event, keyDownStats)
inputHandler?.onRunCommand(mapping.command, key, event, keyDownStats)
}
if (mapping.overrideConsume) consume = mapping.consume
@ -161,17 +158,17 @@ class WKT9: WKT9Interface, InputMethodService() {
var consume = key.consume
val keyDownMS = event.eventTime - event.downTime
val mappings = key.mappings.match(
event = if (keyDownMS >= longPressTimeout) Event.afterLongDown else Event.afterShortDown,
inputMode = inputManager.mode,
event = if (keyDownMS >= 400L) Event.afterLongDown else Event.afterShortDown,
inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputManager.handler?.onRunCommand(mapping.command, key, event, keyUpStats)
inputHandler?.onRunCommand(mapping.command, key, event, keyUpStats)
}
if (mapping.overrideConsume) consume = mapping.consume
@ -192,15 +189,15 @@ class WKT9: WKT9Interface, InputMethodService() {
var consume = key.consume
val mappings = key.mappings.match(
event = Event.keyLongDown,
inputMode = inputManager.mode,
inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputManager.handler?.onRunCommand(mapping.command, key, event, keyDownStats)
inputHandler?.onRunCommand(mapping.command, key, event, keyDownStats)
}
if (mapping.overrideConsume) consume = mapping.consume
@ -213,50 +210,19 @@ class WKT9: WKT9Interface, InputMethodService() {
}
}
override fun onRecord(finishComposing: Boolean) {
if (finishComposing) finishComposingText()
if (!isInputViewShown) requestShowSelf(InputMethodManager.SHOW_IMPLICIT)
record()
}
override fun onReplaceText(text: String) {
currentInputConnection?.run {
beginBatchEdit()
setComposingRegion(0, text.length)
commitText(text, 1)
endBatchEdit()
}
}
override fun onShowInputRequested(flags: Int, configChange: Boolean): Boolean {
return (inputManager.mode != InputMode.Number && inputManager.mode != InputMode.Idle)
return (
inputModeManager.currentMode != InputMode.Number &&
inputModeManager.currentMode != InputMode.Idle
)
}
override fun onStartInput(editorInfo: EditorInfo?, restarting: Boolean) {
if (editorInfo == null) return
val mode = inputModeManager.selectModeByEditor(editorInfo)
inputManager.selectHandler(editorInfo)
}
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {
inputManager.handler?.onUpdateCursorPosition(newSelEnd)
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
switchInputMode(mode)
}
override fun onStartIntent(intent: Intent) {
@ -265,12 +231,28 @@ class WKT9: WKT9Interface, InputMethodService() {
}
}
override fun onSwitchInputHandler(inputMode: InputMode) {
inputManager.switchToHandler(inputMode, cursorPosition)
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo?.let {
selectionStart = it.selectionStart
selectionEnd = it.selectionEnd
composing = if (it.composingTextStart == -1) {
if (composing) finishComposing()
false
} else if (it.selectionEnd != (it.composingTextStart + it.composingText.length)) {
onCommit()
true
} else true
}
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
}
override fun onTranscribe() {
transcribe()
override fun onSwitchInputHandler(inputMode: InputMode) {
val mode = inputModeManager.switchToMode(inputMode)
switchInputMode(mode)
}
override fun onTriggerKeyEvent(event: KeyEvent) {
@ -282,15 +264,21 @@ class WKT9: WKT9Interface, InputMethodService() {
else showStatusIcon(icon)
}
private fun cancelCompose() {
if (!composing) return
override fun onWords(words: List<Word>) {
wordsView?.words = words
}
currentInputConnection?.let {
it.beginBatchEdit()
it.setComposingText("", 1)
it.finishComposingText()
it.endBatchEdit()
}
override fun onWordSelected(word: Word) {
this.onCompose(word.word)
this.inputHandler?.onWordSelected(word)
}
override fun onNextWord() {
wordsView?.next()
}
override fun onPreviousWord() {
wordsView?.previous()
}
private fun deleteText(beforeCursor: Int, afterCursor: Int) {
@ -299,28 +287,23 @@ class WKT9: WKT9Interface, InputMethodService() {
}
}
private fun finishComposingText() {
if (!composing) return
private fun finishComposing() {
wordsView?.clear()
inputHandler?.onFinishComposing()
}
currentInputConnection?.let {
it.finishComposingText()
private fun initializeDictionary(locale: Locale) {
val t9 = T9(this, locale)
inputManager.handler?.onCommitText()
t9.initializeWords()
}
private fun switchInputMode(mode: InputMode) {
inputHandler = when(mode) {
InputMode.Word -> WordInputHandler(this, this, locale)
InputMode.Letter -> LetterInputHandler(this, this, locale)
InputMode.Number -> NumberInputHandler(this, this)
else -> IdleInputHandler(this, this)
}
composing = false
}
private fun record() {
// The recorder must be busy...
if (recorder != null) return
requestShowSelf(InputMethodManager.SHOW_IMPLICIT)
whisper.record()
}
private fun transcribe() {
whisper.transcribe()
}
}

View File

@ -1,70 +0,0 @@
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

@ -14,10 +14,10 @@ interface WordDao {
@Query("DELETE FROM word WHERE word = :word AND locale = :locale")
fun delete(word: String, locale: String)
@Query("SELECT * FROM word WHERE code LIKE :code || '%' " +
@Query("SELECT * FROM word WHERE code LIKE :code || '%' AND locale = :locale " +
"ORDER BY length, weight DESC LIMIT :limit")
suspend fun findCandidates(code: String, limit: Int = 10): List<Word>
suspend fun findCandidates(locale: String, code: String, limit: Int = 10): List<Word>
@Query("UPDATE word SET weight = weight + 1 WHERE word=:word")
suspend fun increaseWeight(word: String)
@Query("UPDATE word SET weight = weight + 1 WHERE id=:id")
suspend fun increaseWeight(id: Int)
}

View File

@ -4,7 +4,13 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(indices = [Index(value = ["word"]), Index(value = ["code"]), Index(value = ["locale"]), Index(value = ["word", "locale"], unique = true)])
@Entity(indices = [
Index(value = ["word"]),
Index(value = ["code"]),
Index(value = ["locale"]),
Index(value = ["word", "locale"], unique = true),
Index(value = ["code", "locale"], unique = false)
])
data class Word(
var word: String,
var code: String,

View File

@ -1,42 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
class IdleInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9, context) {
init {
mode = InputMode.Idle
capMode = null
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.DIAL -> dial()
Command.CAMERA -> triggerKeyEvent(KeyEvent.KEYCODE_CAMERA)
Command.NUMBER -> triggerOriginalKeyEvent(key)
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {
wkt9.onUpdateStatusIcon(R.drawable.idle_en_us_na)
}
private fun dial() {
val uri = "tel:"
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(uri)
wkt9.onStartIntent(intent)
}
}

View File

@ -1,117 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import net.mezimmah.wkt9.keypad.Keypad
open class InputHandler(
override val wkt9: WKT9Interface,
override val context: WKT9
): InputHandlerInterface {
protected val tag = "WKT9"
protected var keypad: Keypad = Keypad(KeyLayout.en_US, KeyLayout.numeric)
override lateinit var mode: InputMode
protected set
override var capMode: Int? = null
protected set
override var cursorPosition: Int = 0
protected set
override fun onCandidateSelected(index: Int) {}
override fun onCommitText() {}
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) {}
override fun onFinish() {}
override fun onInsertText(text: CharSequence) {}
override fun onLongClickCandidate(text: String) {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {}
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {}
override fun onUpdateCursorPosition(cursorPosition: Int) {
this.cursorPosition = cursorPosition
}
protected open fun capMode(key: Key) {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
null
)
var index = modes.indexOf(capMode)
when (key) {
Key.B2 -> {
if (index == 0) index = modes.count()
index--
}
else -> index++
}
capMode = modes[index % modes.count()]
}
protected open fun finalizeWordOrSentence(stats: KeyEventStat) {
val candidates = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
wkt9.onCandidates(
candidates = candidates,
current = stats.repeats % candidates.count()
)
}
protected open fun getCursorPositionInfo(text: CharSequence): CursorPositionInfo {
val trimmed = text.trimEnd()
val regex = "[.!?]$".toRegex()
val startSentence = text.isEmpty() || regex.containsMatchIn(trimmed)
val startWord = text.isEmpty() || (startSentence || trimmed.length < text.length)
return CursorPositionInfo(
startSentence = startSentence,
startWord = startWord
)
}
protected fun getDefaultCapMode(typeFlags: Int): Int? {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
)
modes.forEach {
if (typeFlags.and(it) == it) return it
}
return null
}
protected fun triggerKeyEvent(keyCode: Int) {
val down = KeyEvent(KeyEvent.ACTION_DOWN, keyCode)
val up = KeyEvent(KeyEvent.ACTION_UP, keyCode)
wkt9.onTriggerKeyEvent(down)
wkt9.onTriggerKeyEvent(up)
}
protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode)
}
}

View File

@ -1,34 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
interface InputHandlerInterface {
val wkt9: WKT9Interface
val context: WKT9
val mode: InputMode
val capMode: Int?
val cursorPosition: Int?
fun onCandidateSelected(index: Int)
fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int)
fun onCommitText()
fun onFinish()
fun onInsertText(text: CharSequence)
fun onLongClickCandidate(text: String)
fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat)
fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int)
fun onUpdateCursorPosition(cursorPosition: Int)
}

View File

@ -1,98 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
import android.util.Log
import android.view.inputmethod.EditorInfo
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9
class InputManager(val context: WKT9) {
private val idleInputHandler: IdleInputHandler = IdleInputHandler(context, context)
private val letterInputHandler: LetterInputHandler = LetterInputHandler(context, context)
private val numberInputHandler: NumberInputHandler = NumberInputHandler(context, context)
private val wordInputHandler: WordInputHandler = WordInputHandler(context, context)
private val numericClasses = listOf(
InputType.TYPE_CLASS_DATETIME,
InputType.TYPE_CLASS_NUMBER,
InputType.TYPE_CLASS_PHONE
)
private val letterVariations = listOf(
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_URI,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_FILTER,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD,
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME
)
private var typeClass: Int = 0
private var typeVariation: Int = 0
private var typeFlags: Int = 0
private var allowSuggestions: Boolean = false
var handler: InputHandler? = null
private set
var mode: InputMode = InputMode.Idle
private set
fun selectHandler(editor: EditorInfo) {
val inputType = editor.inputType
val override = selectOverride(editor.packageName)
typeClass = inputType.and(InputType.TYPE_MASK_CLASS)
typeVariation = inputType.and(InputType.TYPE_MASK_VARIATION)
typeFlags = inputType.and(InputType.TYPE_MASK_FLAGS)
allowSuggestions = typeFlags != InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
if (override != null) return switchToHandler(override, editor.initialSelEnd)
val mode = if (numericClasses.contains(typeClass)) {
InputMode.Number
} else if (typeClass == InputType.TYPE_CLASS_TEXT) {
if (letterVariations.contains(typeVariation) || mode == InputMode.Letter) {
InputMode.Letter
} else {
InputMode.Word
}
} else {
InputMode.Idle
}
switchToHandler(mode, editor.initialSelEnd)
}
fun switchToHandler(inputMode: InputMode, cursorPosition: Int) {
val lastHandler = this.handler
val cursor: Int = lastHandler?.cursorPosition ?: cursorPosition
val newHandler = when (inputMode) {
InputMode.Word -> wordInputHandler
InputMode.Letter -> letterInputHandler
InputMode.Number -> numberInputHandler
else -> idleInputHandler
}
newHandler.apply {
onStart(typeClass, typeVariation, typeFlags)
onUpdateCursorPosition(cursor)
}
lastHandler?.onFinish()
this.mode = inputMode
this.handler = newHandler
}
private fun selectOverride(packageName: String): InputMode? {
val numeric = context.resources.getStringArray(R.array.input_mode_numeric)
return if (numeric.contains(packageName)) {
InputMode.Number
} else null
}
}

View File

@ -4,7 +4,7 @@ import net.mezimmah.wkt9.R
enum class InputMode(val icon: Int) {
Word(R.drawable.word_en_us_cap),
Letter(R.drawable.alpha_en_us_cap),
Number(R.drawable.numeric_en_us_num),
Letter(R.drawable.letter_cap),
Number(R.drawable.number_input),
Idle(R.drawable.wkt9)
}

View File

@ -0,0 +1,72 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
import android.view.inputmethod.EditorInfo
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
class InputModeManager(private val context: WKT9IME) {
private val numericClasses = listOf(
InputType.TYPE_CLASS_DATETIME,
InputType.TYPE_CLASS_NUMBER,
InputType.TYPE_CLASS_PHONE
)
private val letterVariations = listOf(
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_URI,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_FILTER,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD,
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME
)
var currentMode: InputMode = InputMode.Idle
private set
private var lastMode: InputMode = InputMode.Idle
fun selectModeByEditor(editor: EditorInfo?): InputMode {
if (editor == null) return switchToMode(InputMode.Idle)
val override = getPackageOverride(editor.packageName)
val inputType = editor.inputType
val typeClass = inputType.and(InputType.TYPE_MASK_CLASS)
val typeVariation = inputType.and(InputType.TYPE_MASK_VARIATION)
if (override != null) return switchToMode(override)
// allowSuggestions = typeFlags != InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
val mode = if (numericClasses.contains(typeClass)) {
InputMode.Number
} else if (typeClass == InputType.TYPE_CLASS_TEXT) {
if (letterVariations.contains(typeVariation) || lastMode == InputMode.Letter) {
InputMode.Letter
} else {
InputMode.Word
}
} else {
InputMode.Idle
}
return switchToMode(mode)
}
fun switchToMode(inputMode: InputMode): InputMode {
lastMode = currentMode
currentMode = inputMode
return inputMode
}
private fun getPackageOverride(packageName: String): InputMode? {
val numeric = context.resources.getStringArray(R.array.input_mode_numeric)
return if (numeric.contains(packageName)) InputMode.Number
else null
}
}

View File

@ -1,314 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.content.Context
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.textservice.SentenceSuggestionsInfo
import android.view.textservice.SpellCheckerSession
import android.view.textservice.SuggestionsInfo
import android.view.textservice.TextServicesManager
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.R
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface
import net.mezimmah.wkt9.dao.WordDao
import net.mezimmah.wkt9.db.AppDatabase
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.exception.MissingLetterCode
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import java.util.Locale
class LetterInputHandler(wkt9: WKT9Interface, context: WKT9): SpellCheckerSession.SpellCheckerSessionListener, InputHandler(wkt9, context) {
private var db: AppDatabase
private var wordDao: WordDao
private var locale: Locale? = 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 wordStart: Boolean = false
private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val timeoutScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var timeoutJob: Job? = null
private val content = StringBuilder()
init {
val textServiceManager = context.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
mode = InputMode.Letter
capMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
db = AppDatabase.getInstance(context)
wordDao = db.getWordDao()
locale = Locale.forLanguageTag("en-US")
spellCheckerSession = textServiceManager.newSpellCheckerSession(
null,
locale,
this,
false
)
Log.d(tag, "Started $mode input mode.")
}
override fun capMode(key: Key) {
super.capMode(key)
updateIcon()
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
if (stats.repeats == 0) {
timeoutJob?.cancel()
clearCandidates(true)
storeLastWord()
}
candidates.addAll(listOf(" ", ". ", "? ", "! ", ", ", ": ", "; "))
wkt9.onCandidates(candidates, stats.repeats % candidates.count())
}
override fun onCandidateSelected(index: Int) {
val candidate = candidates[index]
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)
}
override fun onFinish() {
timeoutJob?.cancel()
clearCandidates(true)
cursorPosition = 0
sentenceStart = false
wordStart = false
content.clear()
}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onInsertText(text: CharSequence) {
clearCandidates(true)
content.replace(cursorPosition, cursorPosition, text.toString())
wkt9.onCompose(text.toString(), cursorPosition, cursorPosition)
wkt9.onFinishComposing(cursorPosition + text.length)
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> capMode(key)
Command.CHARACTER -> composeCharacter(key, stats)
Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor()
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> wkt9.onRecord(true)
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.onTranscribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
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) {
capMode = getDefaultCapMode(typeFlags)
// Get current editor content on start
wkt9.onGetText()?.let {
content.replace(0, content.length, it.toString())
} ?: content.clear()
}
private fun composeCharacter(key: Key, stats: KeyEventStat) {
val layout = KeyLayout.en_US[key] ?: return
val capitalize = Capitalize(capMode)
if (stats.repeats == 0) clearCandidates(true)
layout.forEach {
candidates.add(capitalize.character(it, sentenceStart, wordStart))
}
wkt9.onCandidates(
candidates = candidates,
current = stats.repeats % candidates.count()
)
timeoutJob?.cancel()
timeoutJob = timeoutScope.launch {
delay(400)
clearCandidates(true)
// getSuggestions()
}
}
private fun clearCandidates(finishComposing: Boolean) {
if (finishComposing && candidateIndex != null) {
wkt9.onFinishComposing(cursorPosition)
}
candidateIndex = null
composeRangeStart = null
composeRangeEnd = null
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() {
timeoutJob?.cancel()
clearCandidates(true)
}
private fun storeLastWord() {
val lastWord = getLastWord()
// We're not storing single char words...
if (lastWord.length < 2) return
try {
val codeword = keypad.getCodeForWord(lastWord)
val word = Word(
word = lastWord,
code = codeword,
length = lastWord.length,
weight = 1,
locale = "en_US"
)
queryScope.launch {
wordDao.insert(word)
}
} catch (e: MissingLetterCode) {
Log.d(tag, "Ignoring word because it contains characters unknown.")
}
}
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 fun updateIcon() {
val icon = when (capMode) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.alpha_en_us_upper
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) R.drawable.alpha_en_us_cap else R.drawable.alpha_en_us_lower
else -> R.drawable.alpha_en_us_lower
}
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

@ -1,42 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9Interface
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
class NumberInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9, context) {
init {
mode = InputMode.Number
capMode = null
Log.d(tag, "Started $mode input mode.")
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> capMode(key)
Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key)
Command.SPACE -> finalizeWordOrSentence(stats)
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {
wkt9.onUpdateStatusIcon(R.drawable.numeric_en_us_num)
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Letter)
else wkt9.onSwitchInputHandler(InputMode.Word)
}
private fun delete() {
wkt9.onDeleteText(1, 0, true)
}
}

View File

@ -1,6 +1,6 @@
package net.mezimmah.wkt9.inputmode
data class CursorPositionInfo(
data class TextPositionInfo(
val startWord: Boolean,
val startSentence: Boolean
)

View File

@ -1,294 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
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.WKT9Interface
import net.mezimmah.wkt9.dao.SettingDao
import net.mezimmah.wkt9.dao.WordDao
import net.mezimmah.wkt9.db.AppDatabase
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import net.mezimmah.wkt9.t9.T9
class WordInputHandler(wkt9: WKT9Interface, context: WKT9) : InputHandler(wkt9, context) {
private val content = StringBuilder()
private val codeword = 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 wordDao: WordDao
private var settingDao: SettingDao
private var t9: T9
private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var queryJob: Job? = null
private var staleCodeword = false
private var sentenceStart: Boolean = false
private var wordStart: Boolean = false
init {
mode = InputMode.Word
capMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
db = AppDatabase.getInstance(context)
wordDao = db.getWordDao()
settingDao = db.getSettingDao()
t9 = T9(context, keypad, settingDao, wordDao)
// Todo: Hardcoded language
t9.initializeWords("en_US")
Log.d(tag, "Started $mode input mode.")
}
override fun capMode(key: Key) {
super.capMode(key)
updateIcon()
if (codeword.isNotEmpty()) handleCodewordChange(codeword)
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
if (codeword.isNotEmpty()) commit()
candidates.addAll(listOf(" ", ". ", "? ", "! ", ", ", ": ", "; "))
wkt9.onCandidates(candidates, stats.repeats % candidates.count())
}
override fun onCandidateSelected(index: Int) {
val candidate = candidates[index]
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)
}
override fun onInsertText(text: CharSequence) {
commit()
content.replace(cursorPosition, cursorPosition, text.toString())
wkt9.onCompose(text.toString(), cursorPosition, cursorPosition)
wkt9.onFinishComposing(cursorPosition + text.length)
}
override fun onFinish() {
queryJob?.cancel()
commit()
cursorPosition = 0
sentenceStart = false
wordStart = false
content.clear()
}
override fun onLongClickCandidate(text: String) {
ioScope.launch {
wordDao.delete(text, "en_US")
handleCodewordChange(codeword)
}
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> capMode(key)
Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor()
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.onTranscribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onStart(typeClass: Int, typeVariations: Int, typeFlags: Int) {
capMode = getDefaultCapMode(typeFlags)
// Get current editor content on start
wkt9.onGetText()?.let {
content.replace(0, content.length, it.toString())
} ?: content.clear()
}
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", e)
}
updateIcon()
}
private fun buildCodeword(key: Key) {
// Don't build fruitless codeword
if (staleCodeword) return
if (codeword.isEmpty() && candidateIndex != null) clearCandidates(true)
val code = KeyLayout.numeric[key]
codeword.append(code)
handleCodewordChange(codeword)
}
private fun clearCandidates(finishComposing: Boolean) {
candidateIndex = null
composeRangeStart = null
composeRangeEnd = null
candidates.clear()
wkt9.onClearCandidates()
if (finishComposing) wkt9.onFinishComposing(cursorPosition)
}
private fun clearCodeword(increaseWordWeight: Boolean) {
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() {
staleCodeword = false
if (codeword.length > 1) reduceCodeword()
else if (codeword.isNotEmpty()) undoCodeword()
else if (candidateIndex != null) undoCandidate()
else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, false)
}
}
private fun handleCodewordChange(codeword: StringBuilder) {
queryJob?.cancel()
queryJob = queryScope.launch {
queryT9Candidates(codeword, 25)
if (candidates.isEmpty()) staleCodeword = true
else {
wkt9.onCandidates(
candidates = candidates,
current = 0
)
}
}
}
private fun increaseWordWeight(word: String) {
queryScope.launch {
wordDao.increaseWeight(word)
}
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number)
else wkt9.onSwitchInputHandler(InputMode.Letter)
}
private fun moveCursor() {
commit()
}
private fun record() {
commit()
wkt9.onRecord(true)
}
private fun reduceCodeword() {
codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword)
}
private fun updateIcon() {
val icon = when (capMode) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.word_en_us_upper
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) R.drawable.word_en_us_cap else R.drawable.word_en_us_lower
else -> R.drawable.word_en_us_lower
}
wkt9.onUpdateStatusIcon(icon)
}
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 capitalize = Capitalize(capMode)
candidates.clear()
results.forEach { result ->
candidates.add(capitalize.word(result.word, sentenceStart))
}
}
}

View File

@ -6,6 +6,7 @@ enum class Command {
CHARACTER,
DELETE,
DIAL,
ENTER,
INPUT_MODE,
MOVE_CURSOR,
NUMBER,

View File

@ -6,7 +6,7 @@ data class CommandMapping(
val events: List<Event>? = null,
val inputModes: List<InputMode>? = null,
val packageNames: List<String>? = null,
val alt: Boolean = false,
val fn: Boolean = false,
val ctrl: Boolean = false,
val repeatCount: Int? = null,
val overrideConsume: Boolean = false,

View File

@ -8,6 +8,14 @@ enum class Key(
val consume: Boolean?,
val mappings: Mappings
) {
FN(KeyEvent.KEYCODE_FUNCTION, consume = false, Mappings(
listOf()
)),
CTRL_RIGHT(KeyEvent.KEYCODE_CTRL_RIGHT, consume = false, Mappings(
listOf()
)),
B1(KeyEvent.KEYCODE_BUTTON_1, consume = null, Mappings(
listOf(
CommandMapping(
@ -76,16 +84,22 @@ enum class Key(
N0(KeyEvent.KEYCODE_0, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.SPACE
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -100,16 +114,22 @@ enum class Key(
N1(KeyEvent.KEYCODE_1, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -124,16 +144,22 @@ enum class Key(
N2(KeyEvent.KEYCODE_2, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -148,16 +174,22 @@ enum class Key(
N3(KeyEvent.KEYCODE_3, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -172,16 +204,22 @@ enum class Key(
N4(KeyEvent.KEYCODE_4, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -196,16 +234,22 @@ enum class Key(
N5(KeyEvent.KEYCODE_5, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -220,16 +264,22 @@ enum class Key(
N6(KeyEvent.KEYCODE_6, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -244,16 +294,22 @@ enum class Key(
N7(KeyEvent.KEYCODE_7, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -268,16 +324,22 @@ enum class Key(
N8(KeyEvent.KEYCODE_8, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -292,16 +354,22 @@ enum class Key(
N9(KeyEvent.KEYCODE_9, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
@ -317,38 +385,34 @@ enum class Key(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word, InputMode.Letter, InputMode.Number),
inputModes = listOf(InputMode.Word, InputMode.Letter),
command = Command.DELETE
),
CommandMapping(
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
UP(KeyEvent.KEYCODE_DPAD_UP, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
listOf()
)),
DOWN(KeyEvent.KEYCODE_DPAD_DOWN, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
listOf()
)),
LEFT(KeyEvent.KEYCODE_DPAD_LEFT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word),
command = Command.MOVE_CURSOR,
overrideConsume = true,
consume = true
)
)
)),
@ -356,22 +420,30 @@ enum class Key(
RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word),
command = Command.MOVE_CURSOR,
overrideConsume = true,
consume = true
)
)
)),
ENTER(KeyEvent.KEYCODE_ENTER, consume = null, Mappings(
ENTER(KeyEvent.KEYCODE_ENTER, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.ENTER,
overrideConsume = true,
consume = true
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Idle),
packageNames = listOf("com.android.camera2"),
command = Command.CAMERA,
overrideConsume = true,
consume = true
command = Command.CAMERA
)
)
));

View File

@ -15,15 +15,15 @@ object KeyLayout {
Key.N9 to 9,
)
val en_US = mapOf(
Key.N1 to listOf('.','?','!',',','-','+','=','\'','"','@','$','/','%',':','(',')'),
Key.N2 to listOf('a','b','c','ä','æ','å','à','á','â','ã','ç'),
Key.N3 to listOf('d','e','f','è','é','ê','ë','đ'),
Key.N4 to listOf('g','h','i','ì','í','î','ï'),
Key.N5 to listOf('j','k','l','£'),
Key.N6 to listOf('m','n','o','ö','ø','ò','ó','ô','õ','õ'),
Key.N7 to listOf('p','q','r','s','ß','$'),
Key.N8 to listOf('t','u','v','ù','ú','û','ü'),
Key.N9 to listOf('w','x','y','z','ý','þ')
val chars = mapOf(
Key.N1 to listOf('.','?','!',',','-','+','=','\'','"','@','$','&','/','%',':','(',')','0','1'),
Key.N2 to listOf('a','b','c','ä','æ','å','à','á','â','ã','ç','2','₂','²'),
Key.N3 to listOf('d','e','f','è','é','ê','ë','đ','3','³'),
Key.N4 to listOf('g','h','i','ì','í','î','ï','4'),
Key.N5 to listOf('j','k','l','£','5'),
Key.N6 to listOf('m','n','o','ö','ø','ò','ó','ô','õ','õ','ñ','6'),
Key.N7 to listOf('p','q','r','s','ß','$','7'),
Key.N8 to listOf('t','u','v','ù','ú','û','ü','8'),
Key.N9 to listOf('w','x','y','z','ý','þ','9')
)
}

View File

@ -3,21 +3,17 @@ package net.mezimmah.wkt9.keypad
import net.mezimmah.wkt9.exception.MissingLetterCode
import java.lang.StringBuilder
class Keypad(
private val letterLayout: Map<Key, List<Char>>,
numericLayout: Map<Key, Int>
) {
class Keypad {
private val letterCodeMap: MutableMap<Char, Int> = mutableMapOf()
init {
numericLayout.forEach { (key, code) ->
KeyLayout.numeric.forEach { (key, code) ->
indexKeyLetters(key, code)
}
}
private fun indexKeyLetters(key: Key, code: Int) {
letterLayout[key]?.map { letter ->
KeyLayout.chars[key]?.map { letter ->
letterCodeMap[letter] = code
}
}

View File

@ -7,7 +7,7 @@ class Mappings(private val mappings: List<CommandMapping>) {
event: Event,
inputMode: InputMode,
packageName: String,
alt: Boolean = false,
fn: Boolean = false,
ctrl: Boolean = false,
repeatCount: Int = 0,
): MutableList<CommandMapping>? {
@ -18,7 +18,7 @@ class Mappings(private val mappings: List<CommandMapping>) {
((it.events == null) || it.events.contains(event)) &&
((it.inputModes == null) || it.inputModes.contains(inputMode)) &&
((it.packageNames == null) || it.packageNames.contains(packageName)) &&
(it.alt == alt) &&
(it.fn == fn) &&
(it.ctrl == ctrl) &&
((it.repeatCount == null) || (it.repeatCount == repeatCount))
) commands.add(it)

View File

@ -0,0 +1,114 @@
package net.mezimmah.wkt9.layout
import android.annotation.SuppressLint
import android.app.PendingIntent.getActivity
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.entity.Word
class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private val tag = "WKT9"
private var wkt9: WKT9IME
private var wordCount: Int = 0
private var current: Int = 0
var words: List<Word>? = null
@SuppressLint("InflateParams")
set (words) {
val wordContainer = clear() ?: return
wordCount = 0
words?.forEach { word ->
val view = wkt9.layoutInflater.inflate(R.layout.word, null) as TextView
view.text = word.word
view.setOnClickListener(this)
view.setOnLongClickListener(this)
wordContainer.addView(view)
wordCount++
}
current = 0
field = words
select(current)
}
init {
wkt9 = if (context is ContextThemeWrapper) {
context.baseContext as WKT9IME
} else {
context as WKT9IME
}
}
fun clear(): LinearLayout? {
val wordContainer = findViewById<LinearLayout>(R.id.words)
wordContainer?.removeAllViews()
return wordContainer
}
fun next() {
val next = if (current + 1 >= wordCount) 0 else current + 1
select(next)
}
fun previous() {
val previous = if (current == 0) wordCount - 1 else current - 1
select(previous)
}
override fun onClick(v: View?) {
val words = this.words ?: return
val wordContainer = findViewById<LinearLayout>(R.id.words)
for (i in 0 until wordCount) {
val child: View = wordContainer.getChildAt(i)
if (v != child) {
child.background = null
} else {
child.background = ContextCompat.getDrawable(wkt9, R.drawable.button_radius)
current = i
wkt9.onWordSelected(words[i])
}
}
}
override fun onLongClick(v: View?): Boolean {
Log.d(tag, "We need to delete this word from the db")
return true
}
fun select(index: Int) {
val wordContainer = findViewById<LinearLayout>(R.id.words)
for (i in 0 until wordCount) {
val child: View = wordContainer.getChildAt(i)
if (i != index) continue
onClick(child)
smoothScrollTo(child.left, 0)
}
}
}

View File

@ -34,13 +34,13 @@ class PreferencesFragment: PreferenceFragmentCompat(),
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
findPreference<SwitchPreference>(getString(R.string.overlay_key))?.isChecked = Settings.canDrawOverlays(context)
findPreference<SwitchPreference>(getString(R.string.overlay))?.isChecked = Settings.canDrawOverlays(context)
}
override fun onStart() {
super.onStart()
findPreference<SwitchPreference>(getString(R.string.overlay_key))?.isChecked = Settings.canDrawOverlays(context)
findPreference<SwitchPreference>(getString(R.string.overlay))?.isChecked = Settings.canDrawOverlays(context)
}
override fun onPause() {
@ -53,7 +53,7 @@ class PreferencesFragment: PreferenceFragmentCompat(),
this.key = key
when (key) {
getString(R.string.speech_to_text_key) -> {
getString(R.string.speech_to_text) -> {
if (findPreference<SwitchPreference>(key)?.isChecked == true) {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
@ -66,7 +66,7 @@ class PreferencesFragment: PreferenceFragmentCompat(),
}
}
getString(R.string.overlay_key) -> {
getString(R.string.overlay) -> {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)

View File

@ -7,96 +7,100 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.mezimmah.wkt9.WKT9
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.dao.SettingDao
import net.mezimmah.wkt9.dao.WordDao
import net.mezimmah.wkt9.db.AppDatabase
import net.mezimmah.wkt9.entity.Setting
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.exception.MissingLetterCode
import net.mezimmah.wkt9.keypad.Keypad
import java.util.Locale
class T9 (
private val context: WKT9,
private val keypad: Keypad,
private val settingDao: SettingDao,
private val wordDao: WordDao
private val context: WKT9IME,
private val locale: Locale,
private val batchSize: Int = 1000
) {
// Debugging
private val tag = "WKT9"
private val db: AppDatabase = AppDatabase.getInstance(context)
private val wordDao: WordDao = db.getWordDao()
private val settingDao: SettingDao = db.getSettingDao()
private val keypad: Keypad = Keypad()
// Coroutines
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
fun initializeWords(locale: CharSequence) {
fun initializeWords() {
scope.launch {
val setting = settingDao.getByKey("initialized")
val key = locale.language.plus(":word:initialized")
val setting = settingDao.getByKey(key)
if (setting == null) {
Log.d(tag,"Initializing word database...")
readWords(locale.toString())
Setting.set("initialized", "t", settingDao)
Log.d(tag,"Initializing '$locale' word database.")
readWords()
Setting.set(key, "check", settingDao)
} else Log.d(tag, "Word database already initialized")
}
}
private fun getWord(word: String, weight: Int = 1, locale: String) = Word(
private fun getWord(word: String, locale: String) = Word(
word = word,
code = keypad.getCodeForWord(word),
length = word.length,
weight = weight,
weight = 1,
locale = locale
)
private fun readWords(locale: String) {
val fileName = "$locale/words.txt"
val batchSize = 1000
val wordBatch = ArrayList<Word>(batchSize)
private fun readBatch(batch: Sequence<String>) {
batch.chunked(batchSize).forEach {
readLines(it)
}
}
context.assets.open(fileName).bufferedReader().useLines { lines ->
for (chunk in lines.chunked(batchSize)) {
var arrayListIndex = 0
private fun readLine(line: String): Word? {
return try {
getWord(
word = line.trim(),
locale = locale.language
)
} catch (e: MissingLetterCode) {
Log.d(tag, "Character missing:", e)
wordBatch.clear()
null
}
}
chunk.forEach { line ->
val parts = line.split("\\s".toRegex())
private fun readLines(lines: List<String>) {
val words = mutableListOf<Word>()
try {
wordBatch.add(
arrayListIndex,
lines.forEach { line ->
readLine(line)?.also {
words.add(it)
}
}
when (parts.size) {
// Emoji support
3 -> Word(
word = parts[0],
code = parts[2],
length = 1,
weight = parts[1].toInt(),
locale = locale
)
// Regular dictionary words
else -> getWord(
word = parts[0],
weight = if (parts.size > 1) parts[1].toInt() else 0,
locale = locale
)
}
)
insertWordBatch(words)
}
arrayListIndex++
} catch (ex: MissingLetterCode) {
Log.w(tag, "Problem adding ${parts[0]}: " + ex.message)
}
}
private fun readWords() {
val fileName = locale.language.plus("/words.txt")
runBlocking {
try {
wordDao.insert(*wordBatch.toTypedArray())
} catch (e: SQLiteConstraintException) {
Log.d(tag, "Oh, Brother!")
}
}
context.assets.open(fileName).bufferedReader().useLines {
readBatch(it)
}
}
private fun insertWordBatch(words: List<Word>) {
runBlocking {
try {
wordDao.insert(*words.toTypedArray())
} catch (e: SQLiteConstraintException) {
Log.d(tag, "Oh, Brother!")
}
}
}

View File

@ -11,8 +11,8 @@ 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 net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.inputhandler.InputHandler
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
@ -23,8 +23,8 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
class Whisper(
private val context: WKT9,
private val inputManager: InputManager,
private val context: WKT9IME,
private val inputHandler: InputHandler?,
private val ui: View
) {
private val tag = "WKT9"
@ -59,7 +59,7 @@ class Whisper(
showCandidates()
}
inputManager.handler?.onInsertText(transcription.plus(" "))
// inputHandler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
@ -89,33 +89,33 @@ class Whisper(
}
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
// 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
// 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
// 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() {

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.884,30.784V1.216H14.153V4.501H4.724v9.301h8.363v3.2H4.724V30.784ZM16.951,9.024q1.579,-0.469 3.413,-0.768 1.835,-0.341 3.371,-0.341 1.664,0 3.029,0.469 1.365,0.427 2.304,1.493 0.981,1.067 1.493,2.901 0.555,1.792 0.555,4.48V30.784H27.447V17.557q0,-3.328 -0.853,-4.864 -0.853,-1.536 -3.2,-1.536 -1.237,0 -2.773,0.427V30.784H16.951Z"
android:strokeWidth="0.815"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.303,7.366Q1.121,6.892 1.982,6.203 2.887,5.514 3.705,4.739 4.523,3.92 5.212,3.102 5.944,2.241 6.418,1.423L9.26,1.423L9.26,31.266L5.557,31.266L5.557,7.279Q4.825,8.055 3.834,8.744 2.887,9.433 1.81,10.036ZM17.701,3.274q2.541,-2.541 6.201,-2.541 3.488,0 5.34,1.852 1.852,1.809 1.852,5.685 0,1.809 -0.603,3.445 -0.603,1.636 -1.507,3.187 -0.904,1.507 -1.981,3.015 -1.077,1.507 -2.067,3.101 -0.99,1.55 -1.723,3.273 -0.732,1.68 -0.904,3.617l9.388,0L31.697,31.266L18.347,31.266l0,-0.818q0,-2.885 0.646,-5.125 0.689,-2.239 1.636,-4.048 0.99,-1.809 2.153,-3.359 1.163,-1.55 2.11,-3.015 0.99,-1.507 1.636,-3.058 0.689,-1.593 0.689,-3.488 0,-2.282 -0.904,-3.273 -0.904,-1.034 -2.713,-1.034 -1.249,0 -2.282,0.517Q20.328,5.04 19.51,5.815Z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.948,7.445Q1.759,6.976 2.612,6.293 3.508,5.611 4.319,4.843 5.129,4.032 5.812,3.221 6.537,2.368 7.007,1.557H9.823V31.125H6.153V7.36Q5.428,8.128 4.447,8.811 3.508,9.493 2.441,10.091ZM17.185,3.392q2.517,-2.517 6.144,-2.517 3.456,0 5.291,1.835 1.835,1.792 1.835,5.632 0,1.792 -0.597,3.413 -0.597,1.621 -1.493,3.157 -0.896,1.493 -1.963,2.987 -1.067,1.493 -2.048,3.072 -0.981,1.536 -1.707,3.243 -0.725,1.664 -0.896,3.584h9.301v3.328H17.825v-0.811q0,-2.859 0.64,-5.077 0.683,-2.219 1.621,-4.011 0.981,-1.792 2.133,-3.328 1.152,-1.536 2.091,-2.987 0.981,-1.493 1.621,-3.029 0.683,-1.579 0.683,-3.456 0,-2.261 -0.896,-3.243 -0.896,-1.024 -2.688,-1.024 -1.237,0 -2.261,0.512 -0.981,0.469 -1.792,1.237z"
android:strokeWidth="0.815"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m15.929,15.893q0,3.454 -0.748,5.876 -0.712,2.386 -2.101,3.882 -1.353,1.496 -3.312,2.172 -1.923,0.677 -4.309,0.677 -2.422,0 -5.128,-0.641L0.331,3.928Q1.685,3.643 2.967,3.465 4.249,3.287 5.424,3.287q2.422,0 4.344,0.677 1.959,0.677 3.312,2.172 1.389,1.496 2.101,3.917 0.748,2.386 0.748,5.84zM12.51,15.893q0,-2.635 -0.392,-4.487Q11.727,9.555 10.908,8.379 10.089,7.204 8.735,6.67 7.418,6.136 5.566,6.136q-0.499,0 -0.997,0.036 -0.499,0.036 -1.033,0.107L3.536,25.508q0.534,0.071 1.033,0.107 0.499,0.036 0.961,0.036 1.887,0 3.205,-0.534 1.318,-0.534 2.137,-1.674 0.855,-1.175 1.246,-3.027 0.392,-1.852 0.392,-4.523zM22.766,19.81q0.036,1.353 0.249,2.493 0.214,1.104 0.677,1.959 0.499,0.819 1.282,1.282 0.819,0.463 2.065,0.463 1.033,0 1.887,-0.285 0.89,-0.32 1.282,-0.534l0.57,2.457q-0.463,0.32 -1.567,0.677 -1.068,0.392 -2.564,0.392 -1.959,0 -3.312,-0.712 -1.318,-0.712 -2.172,-1.994 -0.819,-1.282 -1.175,-3.062 -0.356,-1.781 -0.356,-3.917 0,-5.164 1.709,-7.549 1.709,-2.386 4.523,-2.386 3.169,0 4.487,2.386 1.318,2.386 1.318,6.695 0,0.392 0,0.819 0,0.392 -0.036,0.819zM25.864,11.691q-1.531,0 -2.243,1.531 -0.677,1.531 -0.819,4.095l5.662,0q0,-2.6 -0.534,-4.095 -0.534,-1.531 -2.065,-1.531z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M10.238,13.472Q9.101,12.845 7.808,12.845q-0.98,0 -1.725,0.392 -0.745,0.392 -1.294,1.294 -0.51,0.902 -0.784,2.391 -0.274,1.489 -0.274,3.685 0,4.233 1.098,6.076 1.137,1.803 3.41,1.803 0.431,0 0.941,-0.039 0.51,-0.078 1.058,-0.235zM10.238,1.125 L13.609,0.537L13.609,30.405q-1.098,0.47 -2.509,0.745 -1.411,0.314 -2.861,0.314 -4.233,0 -6.154,-2.705 -1.881,-2.705 -1.881,-8.153 0,-2.469 0.392,-4.429 0.431,-1.999 1.294,-3.371 0.902,-1.411 2.273,-2.156 1.372,-0.784 3.253,-0.784 0.902,0 1.529,0.196 0.666,0.157 1.294,0.47zM21.997,21.664q0.039,1.489 0.274,2.744 0.235,1.215 0.745,2.156 0.549,0.902 1.411,1.411 0.902,0.51 2.273,0.51 1.137,0 2.077,-0.314 0.98,-0.353 1.411,-0.588l0.627,2.705q-0.51,0.353 -1.725,0.745 -1.176,0.431 -2.822,0.431 -2.156,0 -3.645,-0.784 -1.45,-0.784 -2.391,-2.195 -0.902,-1.411 -1.294,-3.371 -0.392,-1.96 -0.392,-4.312 0,-5.684 1.881,-8.31 1.881,-2.626 4.978,-2.626 3.489,0 4.939,2.626 1.45,2.626 1.45,7.369 0,0.431 0,0.902 0,0.431 -0.039,0.902zM25.407,12.727q-1.685,0 -2.469,1.685 -0.745,1.685 -0.902,4.508l6.232,0q0,-2.861 -0.588,-4.508 -0.588,-1.685 -2.273,-1.685z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m15.516,16q0,3.353 -0.726,5.703 -0.691,2.316 -2.039,3.767 -1.313,1.452 -3.214,2.108 -1.866,0.657 -4.182,0.657 -2.35,0 -4.977,-0.622L0.377,4.387Q1.691,4.11 2.935,3.937 4.179,3.764 5.32,3.764q2.35,0 4.217,0.657 1.901,0.657 3.214,2.108 1.348,1.452 2.039,3.802 0.726,2.316 0.726,5.668zM12.198,16q0,-2.558 -0.38,-4.355Q11.438,9.848 10.643,8.707 9.848,7.566 8.534,7.048 7.255,6.529 5.458,6.529q-0.484,0 -0.968,0.035 -0.484,0.035 -1.002,0.104L3.488,25.332q0.518,0.069 1.002,0.104 0.484,0.035 0.933,0.035 1.832,0 3.111,-0.518 1.279,-0.518 2.074,-1.625 0.83,-1.141 1.21,-2.938 0.38,-1.797 0.38,-4.39zM20.32,27.994L20.32,4.041l10.749,0l0,2.661l-7.639,0l0,7.431l6.913,0l0,2.592l-6.913,0l0,8.606l8.192,0l0,2.661z"/>
</vector>

View File

@ -5,6 +5,5 @@
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M1.903,28.936V3.064H13.513V5.939H5.263v8.027h7.467v2.8H5.263v9.296H14.111v2.875zM17.703,9.896q1.381,-0.411 2.987,-0.672 1.605,-0.299 2.949,-0.299 1.456,0 2.651,0.411 1.195,0.373 2.016,1.307 0.859,0.933 1.307,2.539 0.485,1.568 0.485,3.92V28.936H26.887V17.363q0,-2.912 -0.747,-4.256 -0.747,-1.344 -2.8,-1.344 -1.083,0 -2.427,0.373v16.8h-3.211z"
android:strokeWidth="0.815"/>
android:pathData="M0.42,29.993L0.42,2.007L12.979,2.007L12.979,5.117L4.055,5.117l0,8.682l8.077,0l0,3.029L4.055,16.828l0,10.055l9.571,0L13.626,29.993ZM18.593,9.397q1.494,-0.444 3.231,-0.727 1.736,-0.323 3.19,-0.323 1.575,0 2.867,0.444 1.292,0.404 2.181,1.413 0.929,1.01 1.413,2.746 0.525,1.696 0.525,4.24l0,12.802l-3.473,0l0,-12.519q0,-3.15 -0.808,-4.604 -0.808,-1.454 -3.029,-1.454 -1.171,0 -2.625,0.404L22.066,29.993l-3.473,0z"/>
</vector>

View File

@ -5,6 +5,5 @@
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m4.908,16.952q0.037,1.419 0.261,2.613 0.224,1.157 0.709,2.053 0.523,0.859 1.344,1.344 0.859,0.485 2.165,0.485 1.083,0 1.979,-0.299 0.933,-0.336 1.344,-0.56l0.597,2.576q-0.485,0.336 -1.643,0.709 -1.12,0.411 -2.688,0.411 -2.053,0 -3.472,-0.747 -1.381,-0.747 -2.277,-2.091 -0.859,-1.344 -1.232,-3.211 -0.373,-1.867 -0.373,-4.107 0,-5.413 1.792,-7.915 1.792,-2.501 4.741,-2.501 3.323,0 4.704,2.501 1.381,2.501 1.381,7.019 0,0.411 0,0.859 0,0.411 -0.037,0.859zM8.156,8.44q-1.605,0 -2.352,1.605 -0.709,1.605 -0.859,4.293h5.936q0,-2.725 -0.56,-4.293Q9.761,8.44 8.156,8.44ZM17.983,6.76q1.381,-0.411 2.987,-0.672 1.605,-0.299 2.949,-0.299 1.456,0 2.651,0.411 1.195,0.373 2.016,1.307 0.859,0.933 1.307,2.539 0.485,1.568 0.485,3.92V25.8H27.167V14.227q0,-2.912 -0.747,-4.256 -0.747,-1.344 -2.8,-1.344 -1.083,0 -2.427,0.373V25.8H17.983Z"
android:strokeWidth="0.815"/>
android:pathData="m3.75,17.006q0.039,1.499 0.276,2.762 0.237,1.223 0.75,2.17 0.552,0.907 1.42,1.42 0.907,0.513 2.288,0.513 1.144,0 2.091,-0.316 0.986,-0.355 1.42,-0.592l0.631,2.722q-0.513,0.355 -1.736,0.75 -1.184,0.434 -2.841,0.434 -2.17,0 -3.669,-0.789Q2.922,25.291 1.975,23.871 1.068,22.45 0.673,20.478q-0.395,-1.973 -0.395,-4.34 0,-5.72 1.894,-8.364 1.894,-2.643 5.01,-2.643 3.511,0 4.971,2.643 1.46,2.643 1.46,7.417 0,0.434 0,0.907 0,0.434 -0.039,0.907zM7.183,8.011q-1.696,0 -2.485,1.696Q3.947,11.404 3.79,14.244l6.273,0q0,-2.88 -0.592,-4.537Q8.879,8.011 7.183,8.011ZM18.624,6.236q1.46,-0.434 3.156,-0.71 1.696,-0.316 3.117,-0.316 1.539,0 2.801,0.434 1.262,0.395 2.13,1.381 0.907,0.986 1.381,2.683 0.513,1.657 0.513,4.142l0,12.506l-3.393,0l0,-12.23q0,-3.077 -0.789,-4.497 -0.789,-1.42 -2.959,-1.42 -1.144,0 -2.564,0.395L22.016,26.356L18.624,26.356Z"/>
</vector>

View File

@ -5,6 +5,5 @@
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.279,28.936L0.279,3.064L11.889,3.064L11.889,5.939L3.639,5.939L3.639,13.965L11.105,13.965v2.8L3.639,16.765v9.296h8.848v2.875zM28.996,28.936Q28.1,26.733 26.943,24.195 25.823,21.619 24.553,19.043 23.321,16.429 22.015,13.891 20.708,11.315 19.439,9.112L19.439,28.936L16.303,28.936L16.303,3.064h2.8q1.419,2.315 2.725,4.741 1.344,2.427 2.539,4.816 1.195,2.352 2.24,4.667 1.083,2.277 1.979,4.331L28.585,3.064h3.136L31.721,28.936Z"
android:strokeWidth="0.815"/>
android:pathData="M0.25,28.56L0.25,3.44L11.523,3.44L11.523,6.231L3.512,6.231l0,7.793l7.25,0l0,2.719L3.512,16.743l0,9.026l8.591,0l0,2.791zM29.104,28.56Q28.234,26.422 27.11,23.957 26.023,21.455 24.79,18.954 23.594,16.417 22.325,13.952 21.057,11.451 19.824,9.312L19.824,28.56L16.779,28.56L16.779,3.44l2.719,0q1.377,2.247 2.646,4.604 1.305,2.356 2.465,4.676 1.16,2.284 2.175,4.531 1.051,2.211 1.921,4.205L28.705,3.44l3.045,0L31.75,28.56Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.626,31.218L0.626,0.2L14.545,0.2L14.545,3.647L4.654,3.647l0,9.623l8.952,0l0,3.357L4.654,16.627l0,11.145l10.608,0l0,3.446zM24.034,28.488q1.701,0 2.551,-0.94 0.895,-0.985 0.895,-2.551 0,-0.985 -0.358,-1.656 -0.358,-0.716 -0.94,-1.253 -0.582,-0.537 -1.343,-0.94 -0.761,-0.448 -1.522,-0.94Q22.557,19.76 21.841,19.178 21.125,18.551 20.543,17.79 20.006,16.985 19.648,16 19.334,14.971 19.334,13.628q0,-2.909 1.79,-4.7 1.835,-1.79 4.923,-1.79 1.298,0 2.462,0.358 1.164,0.313 1.925,0.716l-0.85,3.088q-0.806,-0.448 -1.611,-0.671 -0.806,-0.224 -1.746,-0.224 -1.432,0 -2.283,0.85 -0.85,0.806 -0.85,2.372 0,0.895 0.313,1.567 0.313,0.627 0.806,1.164 0.537,0.492 1.164,0.94 0.671,0.448 1.388,0.85 0.85,0.492 1.656,1.074 0.85,0.582 1.477,1.388 0.671,0.761 1.074,1.835 0.403,1.029 0.403,2.506 0,3.088 -1.835,4.968 -1.79,1.88 -5.281,1.88 -1.79,0 -3.223,-0.492Q19.603,30.815 18.887,30.412l0.806,-3.178q0.671,0.358 1.79,0.806 1.164,0.448 2.551,0.448z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m4.229,17.141q0.045,1.701 0.313,3.133 0.269,1.388 0.85,2.462 0.627,1.029 1.611,1.611 1.029,0.582 2.596,0.582 1.298,0 2.372,-0.358 1.119,-0.403 1.611,-0.671l0.716,3.088q-0.582,0.403 -1.969,0.85 -1.343,0.492 -3.223,0.492 -2.462,0 -4.163,-0.895Q3.289,26.541 2.214,24.929 1.185,23.318 0.737,21.08q-0.448,-2.238 -0.448,-4.923 0,-6.49 2.148,-9.489 2.148,-2.999 5.684,-2.999 3.983,0 5.64,2.999 1.656,2.999 1.656,8.415 0,0.492 0,1.029 0,0.492 -0.045,1.029zM8.123,6.936q-1.925,0 -2.82,1.925Q4.452,10.786 4.273,14.008l7.117,0q0,-3.267 -0.671,-5.147Q10.047,6.936 8.123,6.936ZM24.37,25.019q1.701,0 2.551,-0.94 0.895,-0.985 0.895,-2.551 0,-0.985 -0.358,-1.656 -0.358,-0.716 -0.94,-1.253 -0.582,-0.537 -1.343,-0.94 -0.761,-0.448 -1.522,-0.94Q22.893,16.291 22.177,15.709 21.461,15.082 20.879,14.322 20.342,13.516 19.983,12.531 19.67,11.502 19.67,10.159q0,-2.909 1.79,-4.7 1.835,-1.79 4.923,-1.79 1.298,0 2.462,0.358 1.164,0.313 1.925,0.716l-0.85,3.088q-0.806,-0.448 -1.611,-0.671 -0.806,-0.224 -1.746,-0.224 -1.432,0 -2.283,0.85 -0.85,0.806 -0.85,2.372 0,0.895 0.313,1.567 0.313,0.627 0.806,1.164 0.537,0.492 1.164,0.94 0.671,0.448 1.388,0.85 0.85,0.492 1.656,1.074 0.85,0.582 1.477,1.388 0.671,0.761 1.074,1.835 0.403,1.029 0.403,2.506 0,3.088 -1.835,4.968 -1.79,1.88 -5.281,1.88 -1.79,0 -3.223,-0.492Q19.939,27.346 19.223,26.943l0.806,-3.178q0.671,0.358 1.79,0.806 1.164,0.448 2.551,0.448z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M0.309,30.539L0.309,1.503L13.339,1.503L13.339,4.729L4.08,4.729l0,9.008l8.38,0l0,3.142L4.08,16.88l0,10.433l9.93,0L14.01,30.539ZM23.018,27.941q2.262,0 3.478,-1.257 1.257,-1.257 1.257,-3.394 0,-1.131 -0.377,-2.011 -0.377,-0.88 -1.006,-1.592 -0.587,-0.712 -1.383,-1.299 -0.796,-0.587 -1.634,-1.173 -0.964,-0.67 -2.011,-1.425 -1.047,-0.754 -1.927,-1.76Q18.577,13.025 18.032,11.684 17.487,10.344 17.487,8.542q0,-1.76 0.587,-3.184 0.628,-1.425 1.676,-2.43 1.089,-1.006 2.556,-1.55 1.508,-0.545 3.226,-0.545 1.676,0 3.1,0.419 1.425,0.419 2.388,1.006L29.847,5.232Q28.967,4.646 27.92,4.352 26.914,4.017 25.783,4.017q-2.053,0 -3.31,1.131 -1.215,1.089 -1.215,3.226 0,1.173 0.377,2.053 0.419,0.88 1.047,1.592 0.67,0.712 1.508,1.299 0.88,0.587 1.844,1.215 0.964,0.628 1.969,1.383 1.006,0.754 1.802,1.76 0.838,0.964 1.341,2.304 0.545,1.299 0.545,3.1 0,1.634 -0.503,3.1 -0.503,1.466 -1.592,2.598 -1.047,1.089 -2.681,1.76 -1.592,0.628 -3.771,0.628 -2.011,0 -3.519,-0.461Q18.116,30.245 16.943,29.491L18.116,26.475q1.089,0.67 2.221,1.089 1.173,0.377 2.681,0.377z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M15.158,31.364Q14.195,28.997 12.951,26.269 11.748,23.501 10.384,20.734 9.06,17.926 7.656,15.198 6.252,12.43 4.888,10.063L4.888,31.364L1.519,31.364L1.519,3.564L4.527,3.564Q6.052,6.052 7.456,8.659 8.9,11.266 10.183,13.834 11.467,16.361 12.59,18.848 13.754,21.295 14.716,23.501L14.716,3.564L18.086,3.564L18.086,31.364ZM30.04,31.765Q27.072,31.685 25.788,30.321 24.504,28.917 24.504,25.908L24.504,0.837L27.954,0.235L27.954,25.989q0,1.404 0.562,2.046 0.562,0.602 1.966,0.842z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M3.39,10.908Q4.873,10.467 6.598,10.186 8.322,9.865 9.765,9.865q1.564,0 2.847,0.441 1.283,0.401 2.165,1.403 0.922,1.002 1.403,2.727 0.521,1.684 0.521,4.21L16.702,31.357L13.253,31.357l0,-12.43q0,-3.127 -0.802,-4.571 -0.802,-1.443 -3.007,-1.443 -1.163,0 -2.606,0.401L6.838,31.357L3.39,31.357ZM28.169,31.758q-2.967,-0.08 -4.25,-1.443 -1.283,-1.403 -1.283,-4.411L22.636,0.844L26.084,0.242L26.084,25.984q0,1.403 0.561,2.045 0.561,0.601 1.965,0.842z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M12.297,28.221Q11.45,26.14 10.357,23.741 9.299,21.308 8.1,18.874 6.936,16.406 5.702,14.007 4.467,11.574 3.268,9.493L3.268,28.221L0.305,28.221L0.305,3.779L2.951,3.779Q4.291,5.966 5.525,8.259 6.795,10.551 7.923,12.808 9.052,15.03 10.04,17.217 11.062,19.368 11.909,21.308L11.909,3.779L14.871,3.779L14.871,28.221ZM31.695,25.505L31.695,28.221L20.797,28.221L20.797,3.779l3.174,0L23.971,25.505Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m0.378,1.041q3.093,-0.751 6.054,-0.751 2.298,0 4.154,0.575 1.9,0.53 3.226,1.724 1.37,1.149 2.077,3.005 0.751,1.856 0.751,4.463 0,2.652 -0.751,4.552 -0.751,1.856 -2.121,3.049 -1.37,1.149 -3.314,1.679 -1.9,0.53 -4.243,0.53L4.355,19.867l0,11.358l-3.977,0zM4.355,16.376l1.591,0q1.503,0 2.696,-0.309 1.237,-0.354 2.077,-1.105 0.84,-0.751 1.282,-1.944 0.442,-1.193 0.442,-2.961 0,-1.768 -0.442,-2.961Q11.559,5.858 10.763,5.151 10.012,4.399 8.907,4.09 7.802,3.781 6.476,3.781q-1.149,0 -2.121,0.133zM25.082,8.023l5.833,0L30.915,11.293l-5.833,0l0,12.772q0,2.386 0.84,3.359 0.884,0.928 2.342,0.928 0.751,0 1.414,-0.221 0.707,-0.221 1.193,-0.53l0.751,3.049q-1.812,1.061 -3.845,1.061 -3.27,0 -4.905,-1.812 -1.591,-1.856 -1.591,-6.099L21.281,1.792L25.082,1.129Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m6.431,22.172q1.059,0.584 2.264,0.584 0.913,0 1.607,-0.365 0.694,-0.365 1.169,-1.205 0.511,-0.84 0.767,-2.228 0.256,-1.388 0.256,-3.433 0,-3.945 -1.023,-5.625 -0.986,-1.717 -3.178,-1.717 -0.402,0 -0.877,0.073 -0.475,0.037 -0.986,0.183zM6.431,31.815L3.29,31.815L3.29,6.394Q4.312,5.956 5.591,5.7 6.906,5.408 8.257,5.408q2.009,0 3.433,0.694 1.461,0.694 2.337,2.009 0.913,1.278 1.315,3.178 0.438,1.863 0.438,4.273 0,4.858 -1.571,7.414 -1.534,2.557 -5.04,2.557 -0.84,0 -1.497,-0.183 -0.657,-0.146 -1.242,-0.438zM23.305,5.883l4.821,0L28.126,8.586l-4.821,0l0,10.555q0,1.972 0.694,2.776 0.73,0.767 1.936,0.767 0.621,0 1.169,-0.183 0.584,-0.183 0.986,-0.438l0.621,2.52q-1.497,0.877 -3.178,0.877 -2.703,0 -4.054,-1.497 -1.315,-1.534 -1.315,-5.04L20.164,0.733L23.305,0.185Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="m0.267,3.112q2.709,-0.658 5.302,-0.658 2.013,0 3.638,0.503 1.664,0.464 2.825,1.509 1.2,1.006 1.819,2.632 0.658,1.626 0.658,3.909 0,2.322 -0.658,3.986 -0.658,1.626 -1.858,2.67 -1.2,1.006 -2.903,1.471 -1.664,0.464 -3.715,0.464L3.751,19.599l0,9.947l-3.483,0zM3.751,16.542l1.393,0q1.316,0 2.361,-0.271 1.084,-0.31 1.819,-0.968 0.735,-0.658 1.122,-1.703 0.387,-1.045 0.387,-2.593 0,-1.548 -0.387,-2.593Q10.059,7.331 9.362,6.711 8.705,6.053 7.737,5.783 6.769,5.512 5.608,5.512q-1.006,0 -1.858,0.116zM31.733,2.725L31.733,5.705L25.772,5.705L25.772,29.546L22.289,29.546L22.289,5.705L16.329,5.705L16.329,2.725Z"/>
</vector>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/current_suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp">
<TextView
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_radius"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="500" />
</LinearLayout>

View File

@ -1,77 +0,0 @@
<?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="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom"
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">
<LinearLayout
android:id="@+id/suggestions"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:orientation="horizontal" />
</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>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvContacts"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
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" />

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.Words xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.WKT9"
android:gravity="bottom"
android:background="@color/black">
<LinearLayout
android:id="@+id/words"
android:layout_width="match_parent"
android:layout_height="44dp"
android:orientation="horizontal" />
</net.mezimmah.wkt9.layout.Words>

View File

@ -3,21 +3,10 @@
<string name="app_preferences_name">WKT9 Preferences</string>
<!-- Preference categories -->
<string name="speech_to_text_cat">Speech to Text</string>
<string name="overlay_cat">Speech to Text</string>
<string name="speech_to_text_key">speech_to_text</string>
<string name="speech_to_text_title">Enable Speech to Text</string>
<string name="speech_to_text_summary">For this feature to work WKT9 needs permission to show notifications and record audio. You will be asked to grant these permissions if you haven\'t already granted it.</string>
<string name="whisper_url_key">whisper_url</string>
<string name="whisper_url_title">Whisper Server URL</string>
<string name="whisper_url_summary">Provide an URL to the Whisper server.</string>
<string name="overlay_key">Draw over other activities</string>
<string name="overlay_title">Draw over activities</string>
<string name="overlay_summary">Grant WKT9 permission to draw over other applications</string>
<string name="speech_to_text">speech_to_text</string>
<string name="whisper_url">whisper_url</string>
<string name="overlay">overlay</string>
<string name="compose_timeout">compose_timeout</string>
<string-array name="input_mode_numeric">
<item>org.linphone</item>
@ -26,4 +15,20 @@
<string-array name="camera_apps">
<item>com.android.camera2</item>
</string-array>
<string-array name="timeout_keys">
<item>Very short</item>
<item>Short</item>
<item>Medium</item>
<item>Long</item>
<item>Very long</item>
</string-array>
<string-array name="timeout_values">
<item>300</item>
<item>400</item>
<item>600</item>
<item>800</item>
<item>900</item>
</string-array>
</resources>

View File

@ -8,4 +8,28 @@
android:languageTag="en-US"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="Dutch NL"
android:imeSubtypeLocale="nl_NL"
android:languageTag="nl-NL"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="Spanish ES"
android:imeSubtypeLocale="es_ES"
android:languageTag="es-ES"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="German DE"
android:imeSubtypeLocale="de_DE"
android:languageTag="de-DE"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="Portuguese PT"
android:imeSubtypeLocale="pt_PT"
android:languageTag="pt-PT"
android:imeSubtypeMode="keyboard" />
</input-method>

View File

@ -3,26 +3,37 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:title="@string/speech_to_text_cat" />
app:title="Speech to text" />
<SwitchPreference
app:key="@string/speech_to_text_key"
app:title="@string/speech_to_text_title"
app:summary="@string/speech_to_text_summary" />
app:key="@string/speech_to_text"
app:title="Enable speech to text"
app:summary="Grant WKT9 access to the microphone." />
<EditTextPreference
app:key="@string/whisper_url_key"
app:title="@string/whisper_url_title"
app:summary="@string/whisper_url_summary"
app:dependency="@string/speech_to_text_key" />
app:key="@string/whisper_url"
app:title="Whisper server URL"
app:summary="URL of server that transcribes the recording"
app:dependency="speech_to_text" />
<PreferenceCategory
app:title="@string/overlay_cat" />
app:title="Start other activities" />
<SwitchPreference
app:key="@string/overlay_key"
app:title="@string/overlay_title"
app:summary="@string/overlay_summary" />
app:key="@string/overlay"
app:title="Start other activities"
app:summary="Permit WKT9 to start other activities, like, for example, the dialer." />
<PreferenceCategory
app:title="General settings" />
<DropDownPreference
app:key="@string/compose_timeout"
app:title="Compose timeout"
app:summary="Time before a character gets committed to the editor."
app:entries="@array/timeout_keys"
app:entryValues="@array/timeout_values"
app:defaultValue="400" />
</PreferenceScreen>