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"> android:theme="@style/Theme.WKT9">
<service <service
android:name=".WKT9" android:name=".WKT9IME"
android:label="@string/app_name" android:label="@string/app_name"
android:permission="android.permission.BIND_INPUT_METHOD" android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true"> android:exported="true">

View File

@ -2,39 +2,36 @@ package net.mezimmah.wkt9
import android.content.Intent import android.content.Intent
import android.view.KeyEvent import android.view.KeyEvent
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode import net.mezimmah.wkt9.inputmode.InputMode
interface WKT9Interface { interface IME {
fun onTriggerKeyEvent(event: KeyEvent) fun onTriggerKeyEvent(event: KeyEvent)
fun onStartIntent(intent: Intent) fun onStartIntent(intent: Intent)
fun onCandidates( fun onCandidates(
candidates: List<String>, candidates: ArrayList<CharSequence>,
current: Int? = 0 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 onGetTextBeforeCursor(n: Int): CharSequence?
fun onReplaceText(text: String)
fun onSwitchInputHandler(inputMode: InputMode) fun onSwitchInputHandler(inputMode: InputMode)
fun onRecord(finishComposing: Boolean)
fun onTranscribe()
fun onUpdateStatusIcon(icon: Int?) fun onUpdateStatusIcon(icon: Int?)
} }

View File

@ -3,113 +3,110 @@ package net.mezimmah.wkt9
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.media.MediaRecorder
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import net.mezimmah.wkt9.candidates.Candidates import android.view.inputmethod.InputMethodSubtype
import net.mezimmah.wkt9.inputmode.InputManager 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.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.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 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 wordsView: Words? = null
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 private var composing: Boolean = false
private lateinit var whisper: Whisper private var selectionStart: Int = 0
private var recorder: MediaRecorder? = null private var selectionEnd: Int = 0
private lateinit var candidates: Candidates override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) {
// this.candidates?.load(candidates, current)
override fun onCandidates(candidates: List<String>, current: Int?) {
this.candidates.load(candidates, current)
} }
override fun onCancelCompose() { override fun onCommit(text: CharSequence, beforeCursor: Int, afterCursor: Int) {
cancelCompose()
}
override fun onClearCandidates() {
candidates.clear()
}
override fun onCompose(text: String, rangeStart: Int, rangeEnd: Int) {
val selectionEnd = rangeStart + text.length
currentInputConnection?.run { currentInputConnection?.run {
beginBatchEdit() beginBatchEdit()
setComposingRegion(rangeStart, rangeEnd) setComposingRegion(selectionEnd - beforeCursor, selectionEnd + afterCursor)
commitText(text, 1) setComposingText(text, 1)
setSelection(rangeStart, selectionEnd) finishComposingText()
endBatchEdit() endBatchEdit()
} }
} }
override fun onCompose(text: CharSequence) {
currentInputConnection?.run {
if (!composing) setComposingRegion(selectionStart, selectionEnd)
setComposingText(text, 1)
}
}
@SuppressLint("InflateParams")
override fun onCreate() { override fun onCreate() {
Log.d(tag, "Starting WKT9") 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() super.onCreate()
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreateInputView(): View? { override fun onCreateInputView(): View? {
inputView = layoutInflater.inflate(R.layout.suggestions, null).also { wordsView = layoutInflater.inflate(R.layout.words, null) as Words
candidates = Candidates(this, inputManager, it)
whisper = Whisper(this, inputManager, it) return wordsView
} }
return inputView 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) { override fun onDeleteText(beforeCursor: Int, afterCursor: Int, finishComposing: Boolean) {
if (finishComposing) finishComposingText()
deleteText(beforeCursor, afterCursor) 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? { override fun onGetTextBeforeCursor(n: Int): CharSequence? {
return this.currentInputConnection?.getTextBeforeCursor(n, 0) 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 { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyDownStats.keyCode != keyCode) { if (keyDownStats.keyCode != keyCode) {
keyDownStats.keyCode = keyCode keyDownStats.keyCode = keyCode
@ -124,16 +121,16 @@ class WKT9: WKT9Interface, InputMethodService() {
val hasLongDownMapping = key.mappings.hasLongDownMapping(InputMode.Word) val hasLongDownMapping = key.mappings.hasLongDownMapping(InputMode.Word)
val mappings = key.mappings.match( val mappings = key.mappings.match(
event = if (event.repeatCount > 0) Event.keyDownRepeat else Event.keyDown, event = if (event.repeatCount > 0) Event.keyDownRepeat else Event.keyDown,
inputMode = inputManager.mode, inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName, packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed, fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed, ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount repeatCount = event.repeatCount
) )
mappings?.map { mapping -> mappings?.map { mapping ->
if (mapping.command != null) { 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 if (mapping.overrideConsume) consume = mapping.consume
@ -161,17 +158,17 @@ class WKT9: WKT9Interface, InputMethodService() {
var consume = key.consume var consume = key.consume
val keyDownMS = event.eventTime - event.downTime val keyDownMS = event.eventTime - event.downTime
val mappings = key.mappings.match( val mappings = key.mappings.match(
event = if (keyDownMS >= longPressTimeout) Event.afterLongDown else Event.afterShortDown, event = if (keyDownMS >= 400L) Event.afterLongDown else Event.afterShortDown,
inputMode = inputManager.mode, inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName, packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed, fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed, ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount repeatCount = event.repeatCount
) )
mappings?.map { mapping -> mappings?.map { mapping ->
if (mapping.command != null) { 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 if (mapping.overrideConsume) consume = mapping.consume
@ -192,15 +189,15 @@ class WKT9: WKT9Interface, InputMethodService() {
var consume = key.consume var consume = key.consume
val mappings = key.mappings.match( val mappings = key.mappings.match(
event = Event.keyLongDown, event = Event.keyLongDown,
inputMode = inputManager.mode, inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName, packageName = currentInputEditorInfo.packageName,
alt = event.isAltPressed, fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed ctrl = event.isCtrlPressed
) )
mappings?.map { mapping -> mappings?.map { mapping ->
if (mapping.command != null) { 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 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 { 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) { 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( switchInputMode(mode)
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {
inputManager.handler?.onUpdateCursorPosition(newSelEnd)
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
} }
override fun onStartIntent(intent: Intent) { override fun onStartIntent(intent: Intent) {
@ -265,12 +231,28 @@ class WKT9: WKT9Interface, InputMethodService() {
} }
} }
override fun onSwitchInputHandler(inputMode: InputMode) { override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
inputManager.switchToHandler(inputMode, cursorPosition) 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
} }
override fun onTranscribe() { super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
transcribe() }
override fun onSwitchInputHandler(inputMode: InputMode) {
val mode = inputModeManager.switchToMode(inputMode)
switchInputMode(mode)
} }
override fun onTriggerKeyEvent(event: KeyEvent) { override fun onTriggerKeyEvent(event: KeyEvent) {
@ -282,15 +264,21 @@ class WKT9: WKT9Interface, InputMethodService() {
else showStatusIcon(icon) else showStatusIcon(icon)
} }
private fun cancelCompose() { override fun onWords(words: List<Word>) {
if (!composing) return 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) { private fun deleteText(beforeCursor: Int, afterCursor: Int) {
@ -299,28 +287,23 @@ class WKT9: WKT9Interface, InputMethodService() {
} }
} }
private fun finishComposingText() { private fun finishComposing() {
if (!composing) return wordsView?.clear()
inputHandler?.onFinishComposing()
currentInputConnection?.let {
it.finishComposingText()
inputManager.handler?.onCommitText()
} }
composing = false private fun initializeDictionary(locale: Locale) {
val t9 = T9(this, locale)
t9.initializeWords()
} }
private fun record() { private fun switchInputMode(mode: InputMode) {
// The recorder must be busy... inputHandler = when(mode) {
if (recorder != null) return InputMode.Word -> WordInputHandler(this, this, locale)
InputMode.Letter -> LetterInputHandler(this, this, locale)
requestShowSelf(InputMethodManager.SHOW_IMPLICIT) InputMode.Number -> NumberInputHandler(this, this)
else -> IdleInputHandler(this, this)
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") @Query("DELETE FROM word WHERE word = :word AND locale = :locale")
fun delete(word: String, locale: String) 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") "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") @Query("UPDATE word SET weight = weight + 1 WHERE id=:id")
suspend fun increaseWeight(word: String) suspend fun increaseWeight(id: Int)
} }

View File

@ -4,7 +4,13 @@ import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey 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( data class Word(
var word: String, var word: String,
var code: 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) { enum class InputMode(val icon: Int) {
Word(R.drawable.word_en_us_cap), Word(R.drawable.word_en_us_cap),
Letter(R.drawable.alpha_en_us_cap), Letter(R.drawable.letter_cap),
Number(R.drawable.numeric_en_us_num), Number(R.drawable.number_input),
Idle(R.drawable.wkt9) 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 package net.mezimmah.wkt9.inputmode
data class CursorPositionInfo( data class TextPositionInfo(
val startWord: Boolean, val startWord: Boolean,
val startSentence: 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, CHARACTER,
DELETE, DELETE,
DIAL, DIAL,
ENTER,
INPUT_MODE, INPUT_MODE,
MOVE_CURSOR, MOVE_CURSOR,
NUMBER, NUMBER,

View File

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

View File

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

View File

@ -15,15 +15,15 @@ object KeyLayout {
Key.N9 to 9, Key.N9 to 9,
) )
val en_US = mapOf( val chars = mapOf(
Key.N1 to listOf('.','?','!',',','-','+','=','\'','"','@','$','/','%',':','(',')'), Key.N1 to listOf('.','?','!',',','-','+','=','\'','"','@','$','&','/','%',':','(',')','0','1'),
Key.N2 to listOf('a','b','c','ä','æ','å','à','á','â','ã','ç'), Key.N2 to listOf('a','b','c','ä','æ','å','à','á','â','ã','ç','2','₂','²'),
Key.N3 to listOf('d','e','f','è','é','ê','ë','đ'), Key.N3 to listOf('d','e','f','è','é','ê','ë','đ','3','³'),
Key.N4 to listOf('g','h','i','ì','í','î','ï'), Key.N4 to listOf('g','h','i','ì','í','î','ï','4'),
Key.N5 to listOf('j','k','l','£'), Key.N5 to listOf('j','k','l','£','5'),
Key.N6 to listOf('m','n','o','ö','ø','ò','ó','ô','õ','õ'), Key.N6 to listOf('m','n','o','ö','ø','ò','ó','ô','õ','õ','ñ','6'),
Key.N7 to listOf('p','q','r','s','ß','$'), Key.N7 to listOf('p','q','r','s','ß','$','7'),
Key.N8 to listOf('t','u','v','ù','ú','û','ü'), Key.N8 to listOf('t','u','v','ù','ú','û','ü','8'),
Key.N9 to listOf('w','x','y','z','ý','þ') 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 net.mezimmah.wkt9.exception.MissingLetterCode
import java.lang.StringBuilder import java.lang.StringBuilder
class Keypad( class Keypad {
private val letterLayout: Map<Key, List<Char>>,
numericLayout: Map<Key, Int>
) {
private val letterCodeMap: MutableMap<Char, Int> = mutableMapOf() private val letterCodeMap: MutableMap<Char, Int> = mutableMapOf()
init { init {
numericLayout.forEach { (key, code) -> KeyLayout.numeric.forEach { (key, code) ->
indexKeyLetters(key, code) indexKeyLetters(key, code)
} }
} }
private fun indexKeyLetters(key: Key, code: Int) { private fun indexKeyLetters(key: Key, code: Int) {
letterLayout[key]?.map { letter -> KeyLayout.chars[key]?.map { letter ->
letterCodeMap[letter] = code letterCodeMap[letter] = code
} }
} }

View File

@ -7,7 +7,7 @@ class Mappings(private val mappings: List<CommandMapping>) {
event: Event, event: Event,
inputMode: InputMode, inputMode: InputMode,
packageName: String, packageName: String,
alt: Boolean = false, fn: Boolean = false,
ctrl: Boolean = false, ctrl: Boolean = false,
repeatCount: Int = 0, repeatCount: Int = 0,
): MutableList<CommandMapping>? { ): MutableList<CommandMapping>? {
@ -18,7 +18,7 @@ class Mappings(private val mappings: List<CommandMapping>) {
((it.events == null) || it.events.contains(event)) && ((it.events == null) || it.events.contains(event)) &&
((it.inputModes == null) || it.inputModes.contains(inputMode)) && ((it.inputModes == null) || it.inputModes.contains(inputMode)) &&
((it.packageNames == null) || it.packageNames.contains(packageName)) && ((it.packageNames == null) || it.packageNames.contains(packageName)) &&
(it.alt == alt) && (it.fn == fn) &&
(it.ctrl == ctrl) && (it.ctrl == ctrl) &&
((it.repeatCount == null) || (it.repeatCount == repeatCount)) ((it.repeatCount == null) || (it.repeatCount == repeatCount))
) commands.add(it) ) 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) 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() { override fun onStart() {
super.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() { override fun onPause() {
@ -53,7 +53,7 @@ class PreferencesFragment: PreferenceFragmentCompat(),
this.key = key this.key = key
when (key) { when (key) {
getString(R.string.speech_to_text_key) -> { getString(R.string.speech_to_text) -> {
if (findPreference<SwitchPreference>(key)?.isChecked == true) { if (findPreference<SwitchPreference>(key)?.isChecked == true) {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf( 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) val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent) startActivity(intent)

View File

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

View File

@ -11,8 +11,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
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.WKT9IME
import net.mezimmah.wkt9.inputmode.InputManager import net.mezimmah.wkt9.inputhandler.InputHandler
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -23,8 +23,8 @@ import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class Whisper( class Whisper(
private val context: WKT9, private val context: WKT9IME,
private val inputManager: InputManager, private val inputHandler: InputHandler?,
private val ui: View private val ui: View
) { ) {
private val tag = "WKT9" private val tag = "WKT9"
@ -59,7 +59,7 @@ class Whisper(
showCandidates() showCandidates()
} }
inputManager.handler?.onInsertText(transcription.plus(" ")) // inputHandler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) { } catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e) 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() { private fun showCandidates() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container) // val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container) // val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container) // val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
candidatesView.visibility = View.VISIBLE // candidatesView.visibility = View.VISIBLE
loadingView.visibility = View.GONE // loadingView.visibility = View.GONE
messageView.visibility = View.GONE // messageView.visibility = View.GONE
} }
private fun showMessage() { private fun showMessage() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container) // val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container) // val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container) // val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
candidatesView.visibility = View.GONE // candidatesView.visibility = View.GONE
loadingView.visibility = View.GONE // loadingView.visibility = View.GONE
messageView.visibility = View.VISIBLE // messageView.visibility = View.VISIBLE
} }
private fun showTranscribing() { private fun showTranscribing() {
val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container) // val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container) // val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
val messageView = ui.findViewById<LinearLayout>(R.id.message_container) // val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
candidatesView.visibility = View.GONE // candidatesView.visibility = View.GONE
loadingView.visibility = View.VISIBLE // loadingView.visibility = View.VISIBLE
messageView.visibility = View.GONE // messageView.visibility = View.GONE
} }
private fun stopRecording() { 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"> android:viewportHeight="32">
<path <path
android:fillColor="#FF000000" 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: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"/>
android:strokeWidth="0.815"/>
</vector> </vector>

View File

@ -5,6 +5,5 @@
android:viewportHeight="32"> android:viewportHeight="32">
<path <path
android:fillColor="#FF000000" 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: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"/>
android:strokeWidth="0.815"/>
</vector> </vector>

View File

@ -5,6 +5,5 @@
android:viewportHeight="32"> android:viewportHeight="32">
<path <path
android:fillColor="#FF000000" 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: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"/>
android:strokeWidth="0.815"/>
</vector> </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> <string name="app_preferences_name">WKT9 Preferences</string>
<!-- Preference categories --> <string name="speech_to_text">speech_to_text</string>
<string name="speech_to_text_cat">Speech to Text</string> <string name="whisper_url">whisper_url</string>
<string name="overlay_cat">Speech to Text</string> <string name="overlay">overlay</string>
<string name="compose_timeout">compose_timeout</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-array name="input_mode_numeric"> <string-array name="input_mode_numeric">
<item>org.linphone</item> <item>org.linphone</item>
@ -26,4 +15,20 @@
<string-array name="camera_apps"> <string-array name="camera_apps">
<item>com.android.camera2</item> <item>com.android.camera2</item>
</string-array> </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> </resources>

View File

@ -8,4 +8,28 @@
android:languageTag="en-US" android:languageTag="en-US"
android:imeSubtypeMode="keyboard" /> 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> </input-method>

View File

@ -3,26 +3,37 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory <PreferenceCategory
app:title="@string/speech_to_text_cat" /> app:title="Speech to text" />
<SwitchPreference <SwitchPreference
app:key="@string/speech_to_text_key" app:key="@string/speech_to_text"
app:title="@string/speech_to_text_title" app:title="Enable speech to text"
app:summary="@string/speech_to_text_summary" /> app:summary="Grant WKT9 access to the microphone." />
<EditTextPreference <EditTextPreference
app:key="@string/whisper_url_key" app:key="@string/whisper_url"
app:title="@string/whisper_url_title" app:title="Whisper server URL"
app:summary="@string/whisper_url_summary" app:summary="URL of server that transcribes the recording"
app:dependency="@string/speech_to_text_key" /> app:dependency="speech_to_text" />
<PreferenceCategory <PreferenceCategory
app:title="@string/overlay_cat" /> app:title="Start other activities" />
<SwitchPreference <SwitchPreference
app:key="@string/overlay_key" app:key="@string/overlay"
app:title="@string/overlay_title" app:title="Start other activities"
app:summary="@string/overlay_summary" /> 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> </PreferenceScreen>