Refactored

This commit is contained in:
Nehemiah of Zebulun 2023-11-08 16:15:17 -05:00
parent 3b5b77c540
commit 9d5c6364f0
45 changed files with 1786 additions and 1684 deletions

View File

@ -39,11 +39,12 @@ android {
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.room:room-common:2.5.2")
implementation("androidx.room:room-ktx:2.5.2")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.10")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View File

@ -2,6 +2,10 @@
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@ -23,8 +27,11 @@
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method" />
</service>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
package net.mezimmah.wkt9
import android.content.Intent
import android.view.KeyEvent
import net.mezimmah.wkt9.inputmode.InputMode
interface WKT9Interface {
fun onTriggerKeyEvent(event: KeyEvent)
fun onStartIntent(intent: Intent)
fun onCandidates(
candidates: List<String>,
finishComposing: Boolean = false,
current: Int? = 0,
timeout: Long? = null,
start: Int? = null,
end: Int? = null,
notifyOnChange: Boolean = false
)
fun onCancelCompose()
fun onClearCandidates()
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean)
fun onFinishComposing()
fun onGetText(): CharSequence?
fun onGetTextBeforeCursor(n: Int): CharSequence?
fun onReplaceText(text: String)
fun onSwitchInputHandler(inputMode: InputMode)
fun onRecord(finishComposing: Boolean)
fun onTranscribe()
fun onUpdateStatusIcon(icon: Int?)
}

View File

@ -1,21 +1,18 @@
package net.mezimmah.wkt9.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import net.mezimmah.wkt9.entity.Word
@Dao
interface WordDao {
@Insert
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(vararg words: Word)
@Delete
fun delete(word: Word)
@Query("SELECT * FROM word")
fun getAll(): List<Word>
@Query("DELETE FROM word WHERE word = :word AND locale = :locale")
fun delete(word: String, locale: String)
@Query("SELECT * FROM word WHERE code LIKE :code || '%' " +
"ORDER BY length, weight DESC LIMIT :limit")

View File

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

View File

@ -1,82 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventResult
class AlphaInputMode: BaseInputMode() {
init {
mode = "alpha"
status = Status.CAP
Log.d(tag, "Started $mode input mode.")
}
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
super.onKeyDown(key, composing)
return when(keyCommandResolver.getCommand(key)) {
Command.BACK -> KeyEventResult(consumed = false)
Command.DELETE -> deleteCharacter(composing)
Command.LEFT -> navigateLeft()
Command.RIGHT -> navigateRight()
Command.SELECT -> focus()
else -> KeyEventResult()
}
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, true)) {
Command.RECORD -> record(composing)
Command.NUMBER -> commitNumber(key, composing)
Command.SWITCH_MODE -> switchMode(WKT9InputMode.NUMERIC, composing)
else -> KeyEventResult(true)
}
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, repeat = repeat)) {
Command.HOME -> goHome(repeat, composing)
Command.DELETE -> deleteCharacter(composing)
else -> KeyEventResult()
}
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true)) {
Command.BACK -> goBack(composing)
Command.CHARACTER -> composeCharacter(key, composing)
Command.FN -> functionMode()
Command.SHIFT_MODE -> shiftMode()
Command.SPACE -> finalizeWordOrSentence(composing)
else -> KeyEventResult()
}
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true, longPress = true)) {
Command.TRANSCRIBE -> transcribe(composing)
else -> KeyEventResult()
}
}
override fun composeCharacter(key: Key, composing: Boolean): KeyEventResult {
if (composing && !newKey) return navigateRight()
return super.composeCharacter(key, composing)
}
private fun shiftMode(): KeyEventResult {
status = when(status) {
Status.CAP -> Status.UPPER
Status.UPPER -> Status.LOWER
else -> Status.CAP
}
return KeyEventResult(
consumed = true,
updateInputStatus = true
)
}
}

View File

@ -1,198 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyCommandResolver
import net.mezimmah.wkt9.keypad.KeyEventResult
import net.mezimmah.wkt9.keypad.KeyLayout
open class BaseInputMode: InputMode {
protected var packageName: String? = null
protected val tag = "WKT9"
protected var newKey = true
protected var keyIndex = 0
protected var lastKey: Key? = null
protected open val keyCommandResolver: KeyCommandResolver = KeyCommandResolver.getBasic()
override lateinit var mode: String
protected set
override lateinit var status: Status
protected set
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
keyStats(key)
return KeyEventResult()
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return KeyEventResult()
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return KeyEventResult()
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return KeyEventResult()
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return KeyEventResult()
}
override fun packageName(packageName: String) {
this.packageName = packageName
}
override fun restart() {
Log.d(tag, "Restart should be handled by individual input modes")
}
protected fun commit(text: String, composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = true,
finishComposing = composing,
commit = text
)
}
protected open fun commitNumber(key: Key, composing: Boolean): KeyEventResult {
val number = KeyLayout.numeric[key] ?: return KeyEventResult(true)
return KeyEventResult(
consumed = true,
finishComposing = composing,
commit = number.toString()
)
}
protected open fun composeCharacter(key: Key, composing: Boolean): KeyEventResult {
val layout = KeyLayout.en_US[key] ?: return KeyEventResult(true)
val candidates = layout.map { it.toString() }
return KeyEventResult(
consumed = true,
finishComposing = composing,
startComposing = true,
candidates = candidates,
timeout = 1200
)
}
protected fun composeNumber(key: Key, composing: Boolean): KeyEventResult {
val code = KeyLayout.numeric[key] ?: return KeyEventResult(true)
return KeyEventResult(
consumed = true,
finishComposing = composing,
commit = code.toString()
)
}
protected open fun deleteCharacter(composing: Boolean): KeyEventResult {
return KeyEventResult(
finishComposing = composing,
deleteBeforeCursor = 1
)
}
protected open fun finalizeWordOrSentence(composing: Boolean): KeyEventResult {
if (composing && !newKey) return navigateRight()
return KeyEventResult(
finishComposing = composing,
startComposing = true,
candidates = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; "),
timeout = 700
)
}
protected fun focus(): KeyEventResult {
return KeyEventResult(
consumed = true,
focus = true
)
}
protected fun functionMode(): KeyEventResult {
return KeyEventResult(
consumed = true,
toggleFunctionMode = true
)
}
protected open fun goBack(composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = false,
finishComposing = composing
)
}
protected open fun goHome(repeat: Int, composing: Boolean): KeyEventResult {
if (repeat > 1) return KeyEventResult(true)
return KeyEventResult(
consumed = true,
finishComposing = composing,
goHome = true
)
}
protected open fun navigateLeft(): KeyEventResult {
return KeyEventResult(
consumed = true,
left = true
)
}
protected open fun navigateRight(): KeyEventResult {
return KeyEventResult(
consumed = true,
right = true
)
}
protected open fun record(composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = true,
finishComposing = composing,
record = true
)
}
protected open fun switchMode(mode: WKT9InputMode, composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = true,
finishComposing = composing,
switchInputMode = mode
)
}
protected fun transcribe(composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = true,
finishComposing = composing,
transcribe = true
)
}
private fun keyStats(key: Key) {
when (key != lastKey) {
true -> {
newKey = true
keyIndex = 0
}
false -> {
newKey = false
keyIndex++
}
}
lastKey = key
}
}

View File

@ -0,0 +1,50 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
class Capitalize(
private val capMode: Int?,
private val sentenceDelimiters: List<Char> = listOf('.', ',', '?')
) {
fun text(original: String): String {
var sentenceStart = false
var wordStart = false
var capitalized = original
capitalized.forEachIndexed { index, char ->
if (index == 0) sentenceStart = true
if (char.isLetter()) {
capitalized = if (
capMode == InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS ||
(sentenceStart && (capMode == InputType.TYPE_TEXT_FLAG_CAP_WORDS || capMode == InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)) ||
(wordStart && capMode == InputType.TYPE_TEXT_FLAG_CAP_WORDS)
) {
capitalized.replaceRange(index, index +1, char.uppercase())
} else {
capitalized.replaceRange(index, index +1, char.lowercase())
}
sentenceStart = false
wordStart = false
} else {
if (sentenceDelimiters.contains(char)) sentenceStart = true
if (char.isWhitespace()) wordStart = true
}
}
return capitalized
}
fun word(word: String, sentenceStart: Boolean): String {
return when (capMode) {
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> {
if (sentenceStart) word.replaceFirstChar { it.uppercase() }
else word
}
InputType.TYPE_TEXT_FLAG_CAP_WORDS -> word.replaceFirstChar { it.uppercase() }
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> word.uppercase()
else -> word
}
}
}

View File

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

View File

@ -1,106 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyCommandResolver
import net.mezimmah.wkt9.keypad.KeyEventResult
class FNInputMode: BaseInputMode() {
override val keyCommandResolver: KeyCommandResolver = KeyCommandResolver(
parent = super.keyCommandResolver,
onShort = HashMap(mapOf(
Key.N0 to Command.NEWLINE,
Key.UP to Command.VOL_UP,
Key.DOWN to Command.VOL_DOWN,
Key.LEFT to Command.BRIGHTNESS_DOWN,
Key.RIGHT to Command.BRIGHTNESS_UP
)),
onRepeat = HashMap(mapOf(
Key.N0 to Command.NEWLINE,
Key.UP to Command.VOL_UP,
Key.DOWN to Command.VOL_DOWN,
Key.LEFT to Command.BRIGHTNESS_DOWN,
Key.RIGHT to Command.BRIGHTNESS_UP,
Key.BACK to Command.HOME
))
)
init {
mode = "fn"
status = Status.NA
Log.d(tag, "Started $mode input mode.")
}
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key)) {
Command.BACK -> KeyEventResult(false)
Command.BRIGHTNESS_DOWN -> brightnessDown()
Command.BRIGHTNESS_UP -> brightnessUp()
Command.NEWLINE -> commit("\n", composing)
Command.VOL_UP -> volumeUp()
Command.VOL_DOWN -> volumeDown()
else -> KeyEventResult(true)
}
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return KeyEventResult(true)
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, repeat = repeat)) {
Command.HOME -> goHome(repeat, composing)
Command.BRIGHTNESS_DOWN -> brightnessDown()
Command.BRIGHTNESS_UP -> brightnessUp()
Command.NEWLINE -> commit("\n", composing)
Command.VOL_UP -> volumeUp()
Command.VOL_DOWN -> volumeDown()
else -> KeyEventResult(true)
}
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true)) {
Command.BACK -> goBack(composing)
Command.FN -> functionMode()
else -> KeyEventResult(true)
}
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return KeyEventResult(true)
}
private fun brightnessDown(): KeyEventResult {
return KeyEventResult(
consumed = true,
decreaseBrightness = true
)
}
private fun brightnessUp(): KeyEventResult {
return KeyEventResult(
consumed = true,
increaseBrightness = true
)
}
private fun volumeUp(): KeyEventResult {
return KeyEventResult(
consumed = true,
increaseVolume = true
)
}
private fun volumeDown(): KeyEventResult {
return KeyEventResult(
consumed = true,
decreaseVolume = true
)
}
}

View File

@ -0,0 +1,42 @@
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, false)
Command.NUMBER -> triggerOriginalKeyEvent(key, false)
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,64 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventResult
class IdleInputMode : BaseInputMode() {
init {
mode = "idle"
status = Status.NA
Log.d(tag, "Started $mode input mode.")
}
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key)) {
Command.FN -> KeyEventResult(true)
Command.SELECT -> conditionalSelect()
else -> KeyEventResult(false)
}
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, true)) {
else -> KeyEventResult(false)
}
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, repeat = repeat)) {
Command.HOME -> goHome(repeat, composing)
else -> KeyEventResult(false)
}
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true)) {
Command.BACK -> goBack(composing)
Command.FN -> functionMode()
else -> KeyEventResult(false)
}
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true, longPress = true)) {
else -> KeyEventResult(false)
}
}
private fun conditionalSelect(): KeyEventResult {
return when (packageName) {
"com.android.camera2" -> {
KeyEventResult(
consumed = true,
keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_CAMERA)
)
}
else -> KeyEventResult(consumed = false)
}
}
}

View File

@ -0,0 +1,92 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
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(candidate: String) {
Log.d(tag, "A candidate has been selected: $candidate")
}
override fun onCommitText() {}
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) {}
override fun onFinish() {}
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) {
// capMode = when (key) {
// Key.B2 -> CapMode.previous(capMode)
// else -> CapMode.next(capMode)
// }
}
protected open fun finalizeWordOrSentence(stats: KeyEventStat) {
val candidates = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
wkt9.onCandidates(
candidates = candidates,
finishComposing = stats.repeats == 0,
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 triggerKeyEvent(keyCode: Int, finishComposing: Boolean) {
val down = KeyEvent(KeyEvent.ACTION_DOWN, keyCode)
val up = KeyEvent(KeyEvent.ACTION_UP, keyCode)
if (finishComposing) wkt9.onFinishComposing()
wkt9.onTriggerKeyEvent(down)
wkt9.onTriggerKeyEvent(up)
}
protected fun triggerOriginalKeyEvent(key: Key, finishComposing: Boolean) {
triggerKeyEvent(key.keyCode, finishComposing)
}
}

View File

@ -0,0 +1,32 @@
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(candidate: String)
fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int)
fun onCommitText()
fun onFinish()
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

@ -0,0 +1,89 @@
package net.mezimmah.wkt9.inputmode
import android.text.InputType
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)
val handler = 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(handler)
}
fun switchToHandler(inputMode: InputMode) {
this.handler?.onFinish()
this.mode = inputMode
this.handler = when (inputMode) {
InputMode.Word -> wordInputHandler
InputMode.Letter -> letterInputHandler
InputMode.Number -> numberInputHandler
else -> idleInputHandler
}.apply {
onStart(typeClass, typeVariation, typeFlags)
}
}
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

@ -1,23 +1,10 @@
package net.mezimmah.wkt9.inputmode
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventResult
import net.mezimmah.wkt9.R
interface InputMode {
val mode: String
val status: Status
fun onKeyDown(key: Key, composing: Boolean): KeyEventResult
fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult
fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult
fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult
fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult
fun packageName(packageName: String)
fun restart()
enum class InputMode(val icon: Int) {
Word(R.drawable.word_en_us_cap),
Letter(R.drawable.alpha_en_us_cap),
Number(R.drawable.numeric_en_us_num),
Idle(R.drawable.wkt9)
}

View File

@ -0,0 +1,279 @@
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.TextInfo
import android.view.textservice.TextServicesManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.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
import kotlin.text.StringBuilder
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 var sentenceStart: Boolean = false
private var wordStart: Boolean = false
private var selectionStart: Int = 0
private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val composing = StringBuilder()
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 onCandidateSelected(candidate: String) {
wkt9.onFinishComposing()
}
override fun onCommitText() {
composing.clear()
val info = getCursorPositionInfo(content)
if (info.startSentence || info.startWord) return
val last = content.split("\\s".toRegex()).last()
if (last.length > 2) getSuggestions(last)
}
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) {
val info = getCursorPositionInfo(text)
val lastWord = content.split("\\s+".toRegex()).last()
if (lastWord.isNotEmpty() && (info.startSentence || info.startWord)) {
storeWord(lastWord)
}
content.replace(composingTextStart, composingTextEnd, text.toString())
composing.replace(0, composing.length, text.toString())
}
override fun onFinish() {
super.onFinish()
wkt9.onFinishComposing()
}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
val candidates = mutableListOf<String>()
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
wkt9.onCandidates(
candidates = candidates,
current = null,
start = selectionStart,
end = cursorPosition,
notifyOnChange = true
)
}
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, true)
Command.RECORD -> wkt9.onRecord(true)
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) {
// 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)
var info: CursorPositionInfo
if (cursorPosition > content.length) {
Log.d(tag, "This should not happen and is just a fail over.")
content.replace(0, content.length, wkt9.onGetText().toString())
}
if (cursorPosition == 0) {
info = CursorPositionInfo(
startSentence = true,
startWord = true
)
} else if (composing.isNotEmpty()) {
info = getCursorPositionInfo(composing)
if (!info.startSentence && !info.startWord) {
info = getCursorPositionInfo(content.substring(0, cursorPosition - composing.length))
}
} else {
info = getCursorPositionInfo(content.substring(0, cursorPosition))
}
sentenceStart = info.startSentence
wordStart = info.startWord
updateIcon()
}
private fun getSuggestions(text: String){
val words = arrayOf(
TextInfo(
text.plus("#"), // Add hash to string to get magic performance
0,
text.length + 1, // We added the hash, remember
0,
0
)
)
selectionStart = cursorPosition - text.length
spellCheckerSession?.getSentenceSuggestions(words, 15)
}
private fun moveCursor() {
if (composing.isNotEmpty()) wkt9.onFinishComposing()
}
private fun storeWord(text: String) {
// We're not storing single char words...
if (text.length < 2) return
val words = text.trim().split("\\s+".toRegex())
if (words.count() > 1) {
words.forEach { storeWord(it) }
return
}
try {
val codeword = keypad.getCodeForWord(text)
val word = Word(
word = text,
code = codeword,
length = text.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 composeCharacter(key: Key, stats: KeyEventStat) {
val layout = KeyLayout.en_US[key] ?: return
val capitalize = Capitalize(capMode)
val candidates = mutableListOf<String>()
if (stats.repeats == 0 && composing.isNotEmpty()) wkt9.onFinishComposing()
layout.forEach {
candidates.add(capitalize.word(it.toString(),sentenceStart))
}
wkt9.onCandidates(
candidates = candidates,
current = stats.repeats % candidates.count(),
timeout = 400L
)
}
private fun delete() {
if (composing.isNotEmpty()) {
wkt9.onCancelCompose()
content.delete(cursorPosition - composing.length, cursorPosition)
composing.clear()
} else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, true)
}
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Word)
else wkt9.onSwitchInputHandler(InputMode.Number)
}
private fun updateIcon() {
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)
}
}

View File

@ -0,0 +1,42 @@
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,102 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyCommandResolver
import net.mezimmah.wkt9.keypad.KeyEventResult
class NumericInputMode: BaseInputMode() {
override val keyCommandResolver: KeyCommandResolver = KeyCommandResolver(
parent = super.keyCommandResolver,
onLong = HashMap(mapOf(
Key.N0 to Command.SPACE,
Key.N1 to Command.CHARACTER,
Key.N2 to Command.CHARACTER,
Key.N3 to Command.CHARACTER,
Key.N4 to Command.CHARACTER,
Key.N5 to Command.CHARACTER,
Key.N6 to Command.CHARACTER,
Key.N7 to Command.CHARACTER,
Key.N8 to Command.CHARACTER,
Key.N9 to Command.CHARACTER
)),
afterShort = HashMap(mapOf(
Key.N0 to Command.NUMBER,
Key.N1 to Command.NUMBER,
Key.N2 to Command.NUMBER,
Key.N3 to Command.NUMBER,
Key.N4 to Command.NUMBER,
Key.N5 to Command.NUMBER,
Key.N6 to Command.NUMBER,
Key.N7 to Command.NUMBER,
Key.N8 to Command.NUMBER,
Key.N9 to Command.NUMBER
)),
onRepeat = HashMap(mapOf(
Key.BACK to Command.HOME
))
)
init {
mode = "numeric"
status = Status.NUM
Log.d(tag, "Started $mode input mode.")
}
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
super.onKeyDown(key, composing)
return when(keyCommandResolver.getCommand(key)) {
Command.BACK -> KeyEventResult(consumed = false)
Command.DELETE -> deleteCharacter(composing)
Command.LEFT -> navigateLeft()
Command.RIGHT -> navigateRight()
else -> KeyEventResult()
}
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, true)) {
Command.CHARACTER -> composeCharacter(key, composing)
Command.SPACE -> insertSpace(composing)
Command.SWITCH_MODE -> switchMode(WKT9InputMode.WORD, composing)
else -> KeyEventResult(true)
}
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, repeat = repeat)) {
Command.HOME -> goHome(repeat, composing)
Command.DELETE -> deleteCharacter(composing)
else -> KeyEventResult()
}
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true)) {
Command.BACK -> goBack(composing)
Command.FN -> functionMode()
Command.NUMBER -> composeNumber(key, composing)
else -> KeyEventResult()
}
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true, longPress = true)) {
else -> KeyEventResult()
}
}
private fun insertSpace(composing: Boolean): KeyEventResult {
return KeyEventResult(
consumed = true,
finishComposing = composing,
commit = " "
)
}
}

View File

@ -1,9 +0,0 @@
package net.mezimmah.wkt9.inputmode
enum class Status {
CAP,
UPPER,
LOWER,
NUM,
NA
}

View File

@ -1,9 +0,0 @@
package net.mezimmah.wkt9.inputmode
enum class WKT9InputMode {
WORD,
ALPHA,
NUMERIC,
IDLE,
FN
}

View File

@ -0,0 +1,248 @@
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 composing = StringBuilder()
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 onCommitText() {
if (codeword.isNotEmpty()) increaseWordWeight(composing.toString())
clearCodeword()
}
override fun onComposeText(text: CharSequence, composingTextStart: Int, composingTextEnd: Int) {
content.replace(composingTextStart, composingTextEnd, text.toString())
composing.replace(0, composing.length, text.toString())
}
override fun onFinish() {
if (composing.isNotEmpty()) wkt9.onFinishComposing()
}
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, true)
Command.RECORD -> wkt9.onRecord(true)
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) {
// 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)
if (cursorPosition > content.length) {
Log.d(tag, "This should not happen and is just a fail over.")
content.replace(0, content.length, wkt9.onGetText().toString())
}
var info: CursorPositionInfo
if (cursorPosition == 0) {
info = CursorPositionInfo(
startSentence = true,
startWord = true
)
} else if (composing.isNotEmpty()) {
info = getCursorPositionInfo(composing)
if (!info.startSentence && !info.startWord) {
info = getCursorPositionInfo(content.substring(0, cursorPosition - composing.length))
}
} else {
info = getCursorPositionInfo(content.substring(0, cursorPosition))
}
sentenceStart = info.startSentence
wordStart = info.startWord
updateIcon()
}
private fun buildCodeword(key: Key) {
// Don't build fruitless codeword
if (staleCodeword) return
val code = KeyLayout.numeric[key]
/**
* This happens when some other method than buildCodeword composed text. With, for example,
* finalizeWordOrSentence.
*/
if (codeword.isEmpty()) wkt9.onFinishComposing()
codeword.append(code)
handleCodewordChange(codeword)
}
private fun clearCodeword() {
codeword.clear()
composing.clear()
staleCodeword = false
}
private fun delete() {
staleCodeword = false
if (codeword.length > 1) {
codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword)
} else if (codeword.isNotEmpty()) {
clearCodeword()
wkt9.onCancelCompose()
content.deleteAt(content.length - 1)
} else if (composing.isNotEmpty()) {
wkt9.onCancelCompose()
content.delete(cursorPosition - composing.length, cursorPosition)
composing.clear()
} else if (content.isNotEmpty()) {
content.deleteAt(content.length - 1)
wkt9.onDeleteText(1, 0, true)
}
}
private fun handleCodewordChange(codeword: StringBuilder) {
queryJob?.cancel()
queryJob = queryScope.launch {
val candidates = queryT9Candidates(codeword, 25)
if (candidates.isEmpty()) {
staleCodeword = true
queryJob?.cancel()
wkt9.onClearCandidates()
} 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() {
if (composing.isEmpty()) return
wkt9.onFinishComposing()
wkt9.onClearCandidates()
}
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 suspend fun queryT9Candidates(codeWord: StringBuilder, limit: Int = 10): List<String> {
val results = wordDao.findCandidates(codeWord.toString(), limit)
val capitalize = Capitalize(capMode)
val candidates = mutableListOf<String>()
results.forEach { result ->
candidates.add(capitalize.word(result.word, sentenceStart))
}
return candidates
}
}

View File

@ -1,158 +0,0 @@
package net.mezimmah.wkt9.inputmode
import android.util.Log
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventResult
import net.mezimmah.wkt9.keypad.KeyLayout
import java.lang.StringBuilder
class WordInputMode: BaseInputMode() {
private val codeWord = StringBuilder()
init {
mode = "word"
status = Status.CAP
Log.d(tag, "Started $mode input mode.")
}
override fun onKeyDown(key: Key, composing: Boolean): KeyEventResult {
super.onKeyDown(key, composing)
return when(keyCommandResolver.getCommand(key)) {
Command.BACK -> KeyEventResult(consumed = false)
Command.DELETE -> deleteCharacter(composing)
Command.LEFT -> navigateLeft()
Command.RIGHT -> navigateRight()
Command.SELECT -> focus()
else -> KeyEventResult()
}
}
override fun onKeyLongDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, true)) {
Command.RECORD -> record(composing)
Command.SWITCH_MODE -> switchMode(WKT9InputMode.ALPHA, composing)
Command.NUMBER -> commitNumber(key, composing)
else -> KeyEventResult(true)
}
}
override fun onKeyDownRepeatedly(key: Key, repeat: Int, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, repeat = repeat)) {
Command.HOME -> goHome(repeat, composing)
Command.DELETE -> deleteCharacter(composing)
else -> KeyEventResult()
}
}
override fun afterKeyDown(key: Key, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true)) {
Command.BACK -> goBack(composing)
Command.CHARACTER -> buildCodeWord(key, composing)
Command.FN -> functionMode()
Command.SHIFT_MODE -> shiftMode(composing)
Command.SPACE -> finalizeWordOrSentence(composing)
else -> KeyEventResult()
}
}
override fun afterKeyLongDown(key: Key, keyDownMS: Long, composing: Boolean): KeyEventResult {
return when(keyCommandResolver.getCommand(key, after = true, longPress = true)) {
Command.TRANSCRIBE -> transcribe(composing)
else -> KeyEventResult()
}
}
override fun restart() {
reset()
}
override fun commitNumber(key: Key, composing: Boolean): KeyEventResult {
codeWord.clear()
return super.commitNumber(key, composing)
}
override fun deleteCharacter(composing: Boolean): KeyEventResult {
return if (codeWord.length > 1) {
codeWord.deleteAt(codeWord.length - 1)
KeyEventResult(
codeWord = codeWord
)
} else {
codeWord.clear()
super.deleteCharacter(composing)
}
}
override fun finalizeWordOrSentence(composing: Boolean): KeyEventResult {
codeWord.clear()
return super.finalizeWordOrSentence(composing)
}
override fun goBack(composing: Boolean): KeyEventResult {
reset()
return super.goBack(composing)
}
override fun goHome(repeat: Int, composing: Boolean): KeyEventResult {
reset()
return super.goHome(repeat, composing)
}
override fun record(composing: Boolean): KeyEventResult {
codeWord.clear()
return super.record(composing)
}
override fun switchMode(mode: WKT9InputMode, composing: Boolean): KeyEventResult {
reset()
return super.switchMode(mode, composing)
}
private fun buildCodeWord(key: Key, composing: Boolean): KeyEventResult {
val startComposing = codeWord.isEmpty()
val code = KeyLayout.numeric[key]
codeWord.append(code)
return KeyEventResult(
codeWord = codeWord,
finishComposing = startComposing && composing,
startComposing = startComposing
)
}
private fun reset() {
codeWord.clear()
newKey = true
keyIndex = 0
lastKey = null
}
private fun shiftMode(composing: Boolean): KeyEventResult {
if (!composing) {
status = when(status) {
Status.CAP -> Status.UPPER
Status.UPPER -> Status.LOWER
else -> Status.CAP
}
}
return KeyEventResult(
consumed = true,
updateInputStatus = !composing,
updateWordStatus = composing
)
}
}

View File

@ -1,24 +1,15 @@
package net.mezimmah.wkt9.keypad
enum class Command {
CAMERA,
CAP_MODE,
CHARACTER,
NUMBER,
SPACE,
NEWLINE,
DELETE,
SELECT,
SHIFT_MODE,
SWITCH_MODE,
NAVIGATE,
RIGHT,
LEFT,
VOL_UP,
VOL_DOWN,
BRIGHTNESS_DOWN,
BRIGHTNESS_UP,
DIAL,
INPUT_MODE,
MOVE_CURSOR,
NUMBER,
RECORD,
TRANSCRIBE,
BACK,
HOME,
FN
SPACE,
TRANSCRIBE
}

View File

@ -0,0 +1,15 @@
package net.mezimmah.wkt9.keypad
import net.mezimmah.wkt9.inputmode.InputMode
data class CommandMapping(
val events: List<Event>? = null,
val inputModes: List<InputMode>? = null,
val packageNames: List<String>? = null,
val alt: Boolean = false,
val ctrl: Boolean = false,
val repeatCount: Int? = null,
val overrideConsume: Boolean = false,
val consume: Boolean? = null,
val command: Command? = null,
)

View File

@ -0,0 +1,9 @@
package net.mezimmah.wkt9.keypad;
public enum Event {
keyDown,
keyLongDown,
keyDownRepeat,
afterShortDown,
afterLongDown
}

View File

@ -1,24 +1,384 @@
package net.mezimmah.wkt9.keypad
enum class Key() {
N0,
N1,
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
FN,
STAR,
POUND,
UP,
DOWN,
LEFT,
RIGHT,
SELECT,
DELETE,
BACK,
import android.view.KeyEvent
import net.mezimmah.wkt9.inputmode.InputMode
enum class Key(
val keyCode: Int,
val consume: Boolean?,
val mappings: Mappings
) {
B1(KeyEvent.KEYCODE_BUTTON_1, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Number, InputMode.Word),
ctrl = true,
command = Command.INPUT_MODE
)
)
)),
B2(KeyEvent.KEYCODE_BUTTON_2, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Number, InputMode.Word),
ctrl = true,
command = Command.CAP_MODE
)
)
)),
B3(KeyEvent.KEYCODE_BUTTON_3, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Number, InputMode.Word),
ctrl = true,
command = Command.CAP_MODE
)
)
)),
B4(KeyEvent.KEYCODE_BUTTON_4, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Number, InputMode.Word),
ctrl = true,
command = Command.INPUT_MODE
)
)
)),
CALL(KeyEvent.KEYCODE_CALL, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
command = Command.DIAL
),
CommandMapping(
events = listOf(Event.keyLongDown),
listOf(InputMode.Letter, InputMode.Word),
command = Command.RECORD
),
CommandMapping(
events = listOf(Event.afterLongDown),
listOf(InputMode.Letter, InputMode.Word),
command = Command.TRANSCRIBE
),
)
)),
N0(KeyEvent.KEYCODE_0, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.SPACE
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N1(KeyEvent.KEYCODE_1, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N2(KeyEvent.KEYCODE_2, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N3(KeyEvent.KEYCODE_3, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N4(KeyEvent.KEYCODE_4, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N5(KeyEvent.KEYCODE_5, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N6(KeyEvent.KEYCODE_6, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N7(KeyEvent.KEYCODE_7, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N8(KeyEvent.KEYCODE_8, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N9(KeyEvent.KEYCODE_9, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.NUMBER,
repeatCount = 2
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
DELETE(KeyEvent.KEYCODE_DEL, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word, InputMode.Letter, InputMode.Number),
command = Command.DELETE
)
)
)),
UP(KeyEvent.KEYCODE_DPAD_UP, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
)),
DOWN(KeyEvent.KEYCODE_DPAD_DOWN, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
)),
LEFT(KeyEvent.KEYCODE_DPAD_LEFT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
)),
RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.MOVE_CURSOR
)
)
)),
ENTER(KeyEvent.KEYCODE_ENTER, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Idle),
packageNames = listOf("com.android.camera2"),
command = Command.CAMERA,
overrideConsume = true,
consume = true
)
)
));
companion object {
private val map = Key.values().associateBy(Key::keyCode)
fun fromKeyCode(keyCode: Int) = map[keyCode]
}
}

View File

@ -1,51 +0,0 @@
package net.mezimmah.wkt9.keypad
import java.util.Properties
class KeyCodeMapping(
private val keyMap: Map<Int, Key>,
) {
fun key(keyCode: Int): Key? {
return keyMap[keyCode]
}
companion object {
val basic = mapOf(
4 to Key.BACK,
7 to Key.N0,
8 to Key.N1,
9 to Key.N2,
10 to Key.N3,
11 to Key.N4,
12 to Key.N5,
13 to Key.N6,
14 to Key.N7,
15 to Key.N8,
16 to Key.N9,
17 to Key.STAR,
18 to Key.POUND,
82 to Key.FN,
19 to Key.UP,
20 to Key.DOWN,
21 to Key.LEFT,
22 to Key.RIGHT,
23 to Key.SELECT
)
fun fromProperties(props: Properties): KeyCodeMapping {
val keyMap = HashMap<Int, Key>()
this.basic.forEach {
val keyCode = props.getProperty("key.${it.value.name}")?.toInt() ?: it.key
keyMap[keyCode] = it.value
}
return KeyCodeMapping(keyMap)
}
fun default(): KeyCodeMapping {
return KeyCodeMapping(basic)
}
}
}

View File

@ -1,89 +0,0 @@
package net.mezimmah.wkt9.keypad
class KeyCommandResolver (
private val onShort: HashMap<Key, Command> = HashMap(mapOf()),
private val onLong: HashMap<Key, Command> = HashMap(mapOf()),
private val afterShort: HashMap<Key, Command> = HashMap(mapOf()),
private val afterLong: HashMap<Key, Command> = HashMap(mapOf()),
private val onRepeat: HashMap<Key, Command> = HashMap(mapOf()),
private val parent: KeyCommandResolver? = null
) {
fun getCommand(key: Key, longPress: Boolean = false, after: Boolean = false, repeat: Int = 0): Command? {
val command = when {
repeat > 0 -> onRepeat[key]
(longPress && after) -> afterLong[key]
(longPress) -> onLong[key]
(after) -> afterShort[key]
else -> onShort[key]
}
return when (command) {
null -> parent?.getCommand(key, longPress, after)
else -> command
}
}
companion object {
fun getBasic(): KeyCommandResolver {
return KeyCommandResolver(
onShort = HashMap(mapOf(
Key.BACK to Command.BACK,
Key.LEFT to Command.LEFT,
Key.RIGHT to Command.RIGHT,
Key.UP to Command.NAVIGATE,
Key.DOWN to Command.NAVIGATE,
Key.STAR to Command.DELETE,
Key.SELECT to Command.SELECT,
Key.FN to Command.FN
)),
onLong = HashMap(mapOf(
Key.N0 to Command.NUMBER,
Key.N1 to Command.NUMBER,
Key.N2 to Command.NUMBER,
Key.N3 to Command.NUMBER,
Key.N4 to Command.NUMBER,
Key.N5 to Command.NUMBER,
Key.N6 to Command.NUMBER,
Key.N7 to Command.NUMBER,
Key.N8 to Command.NUMBER,
Key.N9 to Command.NUMBER,
Key.POUND to Command.SWITCH_MODE,
Key.SELECT to Command.RECORD
)),
afterShort = HashMap(mapOf(
Key.N0 to Command.SPACE,
Key.N1 to Command.CHARACTER,
Key.N2 to Command.CHARACTER,
Key.N3 to Command.CHARACTER,
Key.N4 to Command.CHARACTER,
Key.N5 to Command.CHARACTER,
Key.N6 to Command.CHARACTER,
Key.N7 to Command.CHARACTER,
Key.N8 to Command.CHARACTER,
Key.N9 to Command.CHARACTER,
Key.BACK to Command.BACK,
Key.POUND to Command.SHIFT_MODE,
Key.FN to Command.FN
)),
afterLong = HashMap(mapOf(
Key.SELECT to Command.TRANSCRIBE,
)),
onRepeat = HashMap(mapOf(
Key.BACK to Command.HOME,
Key.STAR to Command.DELETE,
))
)
}
}
}

View File

@ -1,33 +0,0 @@
package net.mezimmah.wkt9.keypad
import android.view.KeyEvent
import net.mezimmah.wkt9.inputmode.WKT9InputMode
import java.lang.StringBuilder
data class KeyEventResult(
val consumed: Boolean = true,
val finishComposing: Boolean = false,
val startComposing: Boolean = false,
val increaseWeight: Boolean = false,
val codeWord: StringBuilder? = null,
val candidates: List<String>? = null,
val commit: String? = null,
val timeout: Int? = null,
val deleteBeforeCursor: Int = 0,
val deleteAfterCursor: Int = 0,
val goHome: Boolean = false,
val left: Boolean = false,
val right: Boolean = false,
val record: Boolean = false,
val transcribe: Boolean = false,
val updateInputStatus: Boolean = false,
val updateWordStatus: Boolean = false,
val focus: Boolean = false,
val switchInputMode: WKT9InputMode? = null,
val toggleFunctionMode: Boolean = false,
val increaseVolume: Boolean = false,
val decreaseVolume: Boolean = false,
val increaseBrightness: Boolean = false,
val decreaseBrightness: Boolean = false,
val keyEvent: KeyEvent? = null
)

View File

@ -0,0 +1,6 @@
package net.mezimmah.wkt9.keypad
data class KeyEventStat(
var keyCode: Int,
var repeats: Int = 0
)

View File

@ -24,10 +24,6 @@ object KeyLayout {
Key.N6 to listOf('m','n','o','ö','ø','ò','ó','ô','õ','õ'),
Key.N7 to listOf('p','q','r','s','ß','$'),
Key.N8 to listOf('t','u','v','ù','ú','û','ü'),
Key.N9 to listOf('w','x','y','z','ý','þ'),
Key.STAR to listOf('*'),
Key.POUND to listOf('#'),
Key.N9 to listOf('w','x','y','z','ý','þ')
)
val nonAlphaNumeric = setOf('*','#','\'','"','.','?','!',',','-','@','$','/','%',':','(',')')
}

View File

@ -1,21 +1,16 @@
package net.mezimmah.wkt9.keypad
import android.util.Log
import net.mezimmah.wkt9.exception.MissingLetterCode
import java.lang.StringBuilder
class Keypad(
private val keyCodeMapping: KeyCodeMapping,
private val letterLayout: Map<Key, List<Char>>,
numericLayout: Map<Key, Int>
) {
private val tag = "WKT9"
private val letterCodeMap: MutableMap<Char, Int> = mutableMapOf()
init {
Log.d(tag, "Keypad")
numericLayout.forEach { (key, code) ->
indexKeyLetters(key, code)
}
@ -27,10 +22,6 @@ class Keypad(
}
}
fun getKey(code: Int): Key? {
return keyCodeMapping.key(code)
}
fun getCodeForWord(word: String): String {
val builder = StringBuilder()
val normalized = word.lowercase()
@ -44,7 +35,7 @@ class Keypad(
return builder.toString()
}
fun codeForLetter(letter: Char): Int? {
private fun codeForLetter(letter: Char): Int? {
return letterCodeMap[letter]
}
}

View File

@ -0,0 +1,40 @@
package net.mezimmah.wkt9.keypad
import net.mezimmah.wkt9.inputmode.InputMode
class Mappings(private val mappings: List<CommandMapping>) {
fun match(
event: Event,
inputMode: InputMode,
packageName: String,
alt: Boolean = false,
ctrl: Boolean = false,
repeatCount: Int = 0,
): MutableList<CommandMapping>? {
val commands = mutableListOf<CommandMapping>()
mappings.forEach {
if (
((it.events == null) || it.events.contains(event)) &&
((it.inputModes == null) || it.inputModes.contains(inputMode)) &&
((it.packageNames == null) || it.packageNames.contains(packageName)) &&
(it.alt == alt) &&
(it.ctrl == ctrl) &&
((it.repeatCount == null) || (it.repeatCount == repeatCount))
) commands.add(it)
}
return if (commands.isEmpty()) null else commands
}
fun hasLongDownMapping(inputMode: InputMode): Boolean {
mappings.forEach {
if (
((it.events == null) || it.events.contains(Event.keyLongDown)) &&
((it.inputModes == null) || it.inputModes.contains(inputMode))
) return true
}
return false
}
}

View File

@ -3,7 +3,9 @@ package net.mezimmah.wkt9.preferences
import android.content.SharedPreferences
import android.os.Bundle
import android.Manifest
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.preference.PreferenceFragmentCompat
@ -12,13 +14,12 @@ import net.mezimmah.wkt9.R
class PreferencesFragment: PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private var key: CharSequence? = null
private val tag = "WKT9"
private val requestPermissionLauncher = registerForActivityResult(RequestMultiplePermissions()) { isGranted: Map<String, Boolean> ->
// If any permission got denied we programmatically disable the option
if (isGranted.containsValue(false)) {
val key = getString(R.string.preference_setting_speech_to_text_key)
findPreference<SwitchPreference>(key)?.isChecked = false
findPreference<SwitchPreference>(this.key!!)?.isChecked = false
}
}
@ -32,6 +33,14 @@ class PreferencesFragment: PreferenceFragmentCompat(),
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
findPreference<SwitchPreference>(getString(R.string.overlay_key))?.isChecked = Settings.canDrawOverlays(context)
}
override fun onStart() {
super.onStart()
findPreference<SwitchPreference>(getString(R.string.overlay_key))?.isChecked = Settings.canDrawOverlays(context)
}
override fun onPause() {
@ -41,8 +50,10 @@ class PreferencesFragment: PreferenceFragmentCompat(),
}
override fun onSharedPreferenceChanged(p0: SharedPreferences?, key: String?) {
this.key = key
when (key) {
getString(R.string.preference_setting_speech_to_text_key) -> {
getString(R.string.speech_to_text_key) -> {
if (findPreference<SwitchPreference>(key)?.isChecked == true) {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
@ -54,6 +65,12 @@ class PreferencesFragment: PreferenceFragmentCompat(),
requestPermissionLauncher.launch(permissions)
}
}
getString(R.string.overlay_key) -> {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
}
}
}

View File

@ -1,5 +1,6 @@
package net.mezimmah.wkt9.t9
import android.database.sqlite.SQLiteConstraintException
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -90,7 +91,11 @@ class T9 (
}
runBlocking {
wordDao.insert(*wordBatch.toTypedArray())
try {
wordDao.insert(*wordBatch.toTypedArray())
} catch (e: SQLiteConstraintException) {
Log.d(tag, "Oh, Brother!")
}
}
}
}

View File

@ -1,9 +0,0 @@
package net.mezimmah.wkt9.ui
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
class Suggestions(context: Context, attrs: AttributeSet): RelativeLayout(context, attrs) {
private val tag = "WKT9"
}

View File

@ -10,7 +10,7 @@
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/yellow_radius"
android:background="@drawable/button_radius"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"

View File

@ -9,12 +9,11 @@
<TextView
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/blue_radius"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="500" />
android:textFontWeight="400" />
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout
<HorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -10,52 +10,10 @@
android:background="@color/black"
android:orientation="horizontal">
<HorizontalScrollView
<LinearLayout
android:id="@+id/suggestions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_height="44dp"
android:orientation="horizontal" />
<LinearLayout
android:background="@color/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="3dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="2dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_radius"
android:textColor="@color/button_text"
android:layout_marginEnd="2dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="600"
android:text="En" />
<View
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="4dp"
android:layout_width="2dp"
android:layout_height="match_parent"
android:background="@color/suggestion_text"/>
</LinearLayout>
<LinearLayout
android:id="@+id/suggestions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
</HorizontalScrollView>

View File

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

@ -5,11 +5,11 @@
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="black">#CC000000</color>
<color name="white">#FFFFFFFF</color>
<color name="button">#637783</color>
<color name="button">#CC336699</color>
<color name="button_text">#FFFCF0</color>
<color name="suggestion">#C1E8FF</color>
<color name="current_suggestion">#FFE3C1</color>
<color name="suggestion_text">#424242</color>
<color name="suggestion">#FFFFFFFF</color>
<color name="current_suggestion">#FFFFFFFF</color>
<color name="suggestion_text">#FFFFFFFF</color>
</resources>

View File

@ -3,17 +3,27 @@
<string name="app_preferences_name">WKT9 Preferences</string>
<string name="preference_category_speech_to_text_name">Speech to Text</string>
<!-- Preference categories -->
<string name="speech_to_text_cat">Speech to Text</string>
<string name="overlay_cat">Speech to Text</string>
<string name="preference_setting_speech_to_text_key">speech_to_text</string>
<string name="preference_setting_speech_to_text_title">Enable Speech to Text</string>
<string name="preference_setting_speech_to_text_summary">For this feature to work net.mezimmah.wkt9.WKT9 needs permission to show notifications and record audio. You will be asked to grant these permissions if you haven\'t already permitted it.</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="preference_setting_whisper_url_key">whisper_url</string>
<string name="preference_setting_whisper_url_title">Whisper Server URL</string>
<string name="preference_setting_whisper_url_summary">Provide an URL to the Whisper server.</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">
<item>org.linphone</item>
</string-array>
<string-array name="camera_apps">
<item>com.android.camera2</item>
</string-array>
</resources>

View File

@ -3,17 +3,26 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:title="@string/preference_category_speech_to_text_name" />
app:title="@string/speech_to_text_cat" />
<SwitchPreference
app:key="@string/preference_setting_speech_to_text_key"
app:title="@string/preference_setting_speech_to_text_title"
app:summary="@string/preference_setting_speech_to_text_summary" />
app:key="@string/speech_to_text_key"
app:title="@string/speech_to_text_title"
app:summary="@string/speech_to_text_summary" />
<EditTextPreference
app:key="@string/preference_setting_whisper_url_key"
app:title="@string/preference_setting_whisper_url_title"
app:summary="@string/preference_setting_whisper_url_summary"
app:dependency="@string/preference_setting_speech_to_text_key" />
app:key="@string/whisper_url_key"
app:title="@string/whisper_url_title"
app:summary="@string/whisper_url_summary"
app:dependency="@string/speech_to_text_key" />
<PreferenceCategory
app:title="@string/overlay_cat" />
<SwitchPreference
app:key="@string/overlay_key"
app:title="@string/overlay_title"
app:summary="@string/overlay_summary" />
</PreferenceScreen>