From 9d5c6364f0ee4ea869a9994c86caa2c76716b382 Mon Sep 17 00:00:00 2001 From: Nehemiah Date: Wed, 8 Nov 2023 16:15:17 -0500 Subject: [PATCH] Refactored --- app/build.gradle.kts | 3 +- app/src/main/AndroidManifest.xml | 7 + app/src/main/java/net/mezimmah/wkt9/WKT9.kt | 907 ++++++------------ .../java/net/mezimmah/wkt9/WKT9Interface.kt | 43 + .../java/net/mezimmah/wkt9/dao/WordDao.kt | 11 +- .../java/net/mezimmah/wkt9/entity/Word.kt | 2 +- .../mezimmah/wkt9/inputmode/AlphaInputMode.kt | 82 -- .../mezimmah/wkt9/inputmode/BaseInputMode.kt | 198 ---- .../net/mezimmah/wkt9/inputmode/Capitalize.kt | 50 + .../wkt9/inputmode/CursorPositionInfo.kt | 6 + .../mezimmah/wkt9/inputmode/FNInputMode.kt | 106 -- .../wkt9/inputmode/IdleInputHandler.kt | 42 + .../mezimmah/wkt9/inputmode/IdleInputMode.kt | 64 -- .../mezimmah/wkt9/inputmode/InputHandler.kt | 92 ++ .../wkt9/inputmode/InputHandlerInterface.kt | 32 + .../mezimmah/wkt9/inputmode/InputManager.kt | 89 ++ .../net/mezimmah/wkt9/inputmode/InputMode.kt | 25 +- .../wkt9/inputmode/LetterInputHandler.kt | 279 ++++++ .../wkt9/inputmode/NumberInputHandler.kt | 42 + .../wkt9/inputmode/NumericInputMode.kt | 102 -- .../net/mezimmah/wkt9/inputmode/Status.kt | 9 - .../mezimmah/wkt9/inputmode/WKT9InputMode.kt | 9 - .../wkt9/inputmode/WordInputHandler.kt | 248 +++++ .../mezimmah/wkt9/inputmode/WordInputMode.kt | 158 --- .../java/net/mezimmah/wkt9/keypad/Command.kt | 25 +- .../mezimmah/wkt9/keypad/CommandMapping.kt | 15 + .../java/net/mezimmah/wkt9/keypad/Event.java | 9 + .../main/java/net/mezimmah/wkt9/keypad/Key.kt | 402 +++++++- .../mezimmah/wkt9/keypad/KeyCodeMapping.kt | 51 - .../wkt9/keypad/KeyCommandResolver.kt | 89 -- .../mezimmah/wkt9/keypad/KeyEventResult.kt | 33 - .../net/mezimmah/wkt9/keypad/KeyEventStat.kt | 6 + .../net/mezimmah/wkt9/keypad/KeyLayout.kt | 6 +- .../java/net/mezimmah/wkt9/keypad/Keypad.kt | 11 +- .../java/net/mezimmah/wkt9/keypad/Mappings.kt | 40 + .../wkt9/preferences/PreferencesFragment.kt | 25 +- app/src/main/java/net/mezimmah/wkt9/t9/T9.kt | 7 +- .../java/net/mezimmah/wkt9/ui/Suggestions.kt | 9 - .../main/res/layout/current_suggestion.xml | 2 +- app/src/main/res/layout/suggestion.xml | 5 +- app/src/main/res/layout/suggestions.xml | 54 +- app/src/main/res/layout/symbols.xml | 16 + app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/strings.xml | 24 +- app/src/main/res/xml/preferences.xml | 25 +- 45 files changed, 1786 insertions(+), 1684 deletions(-) create mode 100644 app/src/main/java/net/mezimmah/wkt9/WKT9Interface.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/BaseInputMode.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/Capitalize.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/CursorPositionInfo.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/FNInputMode.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputHandler.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputMode.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandler.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandlerInterface.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/InputManager.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/LetterInputHandler.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/NumberInputHandler.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/WKT9InputMode.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputHandler.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/CommandMapping.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/Event.java delete mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/KeyCodeMapping.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventStat.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/keypad/Mappings.kt delete mode 100644 app/src/main/java/net/mezimmah/wkt9/ui/Suggestions.kt create mode 100644 app/src/main/res/layout/symbols.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72eb333..3276994 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 446fcec..576ba0a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + @@ -23,8 +27,11 @@ android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true"> + + + diff --git a/app/src/main/java/net/mezimmah/wkt9/WKT9.kt b/app/src/main/java/net/mezimmah/wkt9/WKT9.kt index 862003e..3c66874 100644 --- a/app/src/main/java/net/mezimmah/wkt9/WKT9.kt +++ b/app/src/main/java/net/mezimmah/wkt9/WKT9.kt @@ -3,148 +3,86 @@ package net.mezimmah.wkt9 import android.annotation.SuppressLint import android.content.Intent import android.inputmethodservice.InputMethodService -import android.media.AudioManager import android.media.MediaRecorder -import android.net.Uri -import android.os.Bundle import android.provider.Settings -import android.text.InputType import android.util.Log import android.view.KeyEvent import android.view.View -import android.view.ViewConfiguration import android.view.inputmethod.EditorInfo import android.view.inputmethod.ExtractedTextRequest -import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InputMethodManager -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 android.widget.LinearLayout import android.widget.TextView import android.widget.Toast -import android.widget.inline.InlinePresentationSpec 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.dao.SettingDao -import net.mezimmah.wkt9.dao.WordDao -import net.mezimmah.wkt9.db.AppDatabase -import net.mezimmah.wkt9.inputmode.AlphaInputMode -import net.mezimmah.wkt9.inputmode.FNInputMode -import net.mezimmah.wkt9.inputmode.IdleInputMode +import net.mezimmah.wkt9.inputmode.InputManager import net.mezimmah.wkt9.inputmode.InputMode -import net.mezimmah.wkt9.inputmode.NumericInputMode -import net.mezimmah.wkt9.inputmode.Status -import net.mezimmah.wkt9.inputmode.WKT9InputMode -import net.mezimmah.wkt9.inputmode.WordInputMode -import net.mezimmah.wkt9.keypad.KeyCodeMapping -import net.mezimmah.wkt9.keypad.KeyEventResult -import net.mezimmah.wkt9.keypad.KeyLayout -import net.mezimmah.wkt9.keypad.Keypad -import net.mezimmah.wkt9.t9.T9 +import net.mezimmah.wkt9.keypad.Event +import net.mezimmah.wkt9.keypad.Key +import net.mezimmah.wkt9.keypad.KeyEventStat import net.mezimmah.wkt9.voice.Whisper import okio.IOException import java.io.File -import java.util.Locale - -class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListener { +class WKT9: WKT9Interface, InputMethodService() { private val tag = "WKT9" + private val longPressTimeout = 400L - // Dao - Database - private lateinit var db: AppDatabase - private lateinit var wordDao: WordDao - private lateinit var settingDao: SettingDao + private lateinit var inputManager: InputManager - // Coroutines - private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private var queryJob: Job? = null - private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private var ioJob: Job? = null - private val commitScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private var commitJob: Job? = null - - private var cursorPosition = 0 - private var longPressTimeout = 700 - - // Keypad - private lateinit var keypad: Keypad - - // T9 - private lateinit var t9: T9 - - // Input - private var languageTag = "en_US" - private var lastInputMode: WKT9InputMode = WKT9InputMode.WORD - private var inputMode: InputMode? = null - private lateinit var alphaInputMode: AlphaInputMode - private lateinit var fnInputMode: FNInputMode - private lateinit var numericInputMode: NumericInputMode - private lateinit var wordInputMode: WordInputMode - private lateinit var idleInputMode: IdleInputMode - private var composing = false - private val candidates: MutableList = mutableListOf() - private var candidateIndex = 0 - private var inputStatus: Status = Status.CAP - private var timeout: Int? = null - private var lastComposedString: String? = null - private val commitHistory: MutableList = mutableListOf() - - // Spell checker - private lateinit var locale: Locale - private var allowSuggestions = false - private var spellCheckerSession: SpellCheckerSession? = null - - // UI private var inputView: View? = null - private var toast: Toast? = null + + private var composing: Boolean = false + private var lastComposedText: CharSequence = "" + private var cursorPosition: Int = 0 + + private val keyDownStats = KeyEventStat(0, 0) + private val keyUpStats = KeyEventStat(0, 0) // Whisper private val whisper: Whisper = Whisper() private var recorder: MediaRecorder? = null private var recording: File? = null - override fun onCreate() { - val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - val inputMethodSubtype = inputMethodManager.currentInputMethodSubtype + private var toast: Toast? = null - locale = inputMethodSubtype?.let { - Locale.forLanguageTag(it.languageTag) - } ?: Locale.forLanguageTag("en-US") + private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var ioJob: Job? = null - Log.d(tag, "WKT9 is loading: $locale") + private val commitScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private var commitJob: Job? = null - db = AppDatabase.getInstance(this) - wordDao = db.getWordDao() - settingDao = db.getSettingDao() - keypad = Keypad(KeyCodeMapping(KeyCodeMapping.basic), KeyLayout.en_US, KeyLayout.numeric) - t9 = T9(this, keypad, settingDao, wordDao) - alphaInputMode = AlphaInputMode() - fnInputMode = FNInputMode() - numericInputMode = NumericInputMode() - wordInputMode = WordInputMode() - idleInputMode = IdleInputMode() - longPressTimeout = ViewConfiguration.getLongPressTimeout() - lastComposedString = null - commitHistory.clear() + override fun onCandidates( + candidates: List, + finishComposing: Boolean, + current: Int?, + timeout: Long?, + start: Int?, + end: Int?, + notifyOnChange: Boolean + ) { + if (finishComposing) finishComposingText() - t9.initializeWords(languageTag) - - super.onCreate() + loadCandidates(candidates, current, timeout, start, end, notifyOnChange) } - override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? { - Log.d(tag, "Here we are") + override fun onCancelCompose() { + cancelCompose() + } - return InlineSuggestionsRequest.Builder(ArrayList()) - .setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED) - .build() + override fun onClearCandidates() { + clearCandidates() + } + + override fun onCreate() { + inputManager = InputManager(this) + + super.onCreate() } @SuppressLint("InflateParams") @@ -154,18 +92,14 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene return inputView } - override fun onFinishInput() { - super.onFinishInput() + override fun onDeleteText(beforeCursor: Int, afterCursor: Int, finishComposing: Boolean) { + if (finishComposing) finishComposingText() - clearCandidates() + deleteText(beforeCursor, afterCursor) + } - spellCheckerSession?.cancel() - spellCheckerSession?.close() - - inputMode = null - cursorPosition = 0 - inputStatus = Status.CAP - spellCheckerSession = null + override fun onFinishComposing() { + finishComposingText() } override fun onFinishInputView(finishingInput: Boolean) { @@ -174,52 +108,147 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene clearCandidates() } + override fun onGetTextBeforeCursor(n: Int): CharSequence? { + return this.currentInputConnection?.getTextBeforeCursor(n, 0) + } + + override fun onGetText(): CharSequence? { + val request = ExtractedTextRequest() + val text = currentInputConnection.getExtractedText(request, 0) + + return text?.text + } + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - val key = keypad.getKey(keyCode) ?: return super.onKeyDown(keyCode, event) - val repeatCount = event?.repeatCount ?: 0 + if (keyDownStats.keyCode != keyCode) { + keyDownStats.keyCode = keyCode + keyDownStats.repeats = 0 + } else keyDownStats.repeats++ - return inputMode?.let { - val keyEventResult = - if (repeatCount > 0) it.onKeyDownRepeatedly(key, repeatCount, composing) - else { - event?.startTracking() - it.onKeyDown(key, composing) - } + val key = Key.fromKeyCode(keyCode) - handleKeyEventResult(keyEventResult) - } ?: super.onKeyDown(keyCode, event) + if (event == null || key == null) return super.onKeyDown(keyCode, event) + + var consume = key.consume + val hasLongDownMapping = key.mappings.hasLongDownMapping(InputMode.Word) + val mappings = key.mappings.match( + event = if (event.repeatCount > 0) Event.keyDownRepeat else Event.keyDown, + inputMode = inputManager.mode, + packageName = currentInputEditorInfo.packageName, + alt = event.isAltPressed, + ctrl = event.isCtrlPressed, + repeatCount = event.repeatCount + ) + + mappings?.map { mapping -> + if (mapping.command != null) { + inputManager.handler?.onRunCommand(mapping.command, key, event, keyDownStats) + } + + if (mapping.overrideConsume) consume = mapping.consume + } + + if (hasLongDownMapping && event.repeatCount == 0) event.startTracking() + + return when (consume) { + true -> true + false -> false + else -> super.onKeyDown(keyCode, event) + } } override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - val key = keypad.getKey(keyCode) ?: return super.onKeyUp(keyCode, event) - val eventTime = event?.eventTime ?: 0 - val downTime = event?.downTime ?: 0 - val keyDownMS = eventTime - downTime + if (keyUpStats.keyCode != keyCode) { + keyUpStats.keyCode = keyCode + keyUpStats.repeats = 0 + } else keyUpStats.repeats++ - return inputMode?.let { - val keyEventResult = - if (keyDownMS >= longPressTimeout) it.afterKeyLongDown(key, keyDownMS, composing) - else it.afterKeyDown(key, composing) + val key = Key.fromKeyCode(keyCode) - handleKeyEventResult(keyEventResult) - } ?: super.onKeyUp(keyCode, event) + if (event == null || key == null) return super.onKeyUp(keyCode, event) + + var consume = key.consume + val keyDownMS = event.eventTime - event.downTime + val mappings = key.mappings.match( + event = if (keyDownMS >= longPressTimeout) Event.afterLongDown else Event.afterShortDown, + inputMode = inputManager.mode, + packageName = currentInputEditorInfo.packageName, + alt = event.isAltPressed, + ctrl = event.isCtrlPressed, + repeatCount = event.repeatCount + ) + + mappings?.map { mapping -> + if (mapping.command != null) { + inputManager.handler?.onRunCommand(mapping.command, key, event, keyUpStats) + } + + if (mapping.overrideConsume) consume = mapping.consume + } + + return when (consume) { + true -> true + false -> false + else -> super.onKeyUp(keyCode, event) + } } override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { - val key = keypad.getKey(keyCode) ?: return super.onKeyLongPress(keyCode, event) + val key = Key.fromKeyCode(keyCode) - return inputMode?.let { - val keyEventResult = it.onKeyLongDown(key, composing) + if (event == null || key == null) return super.onKeyLongPress(keyCode, event) - handleKeyEventResult(keyEventResult) - } ?: super.onKeyLongPress(keyCode, event) + var consume = key.consume + val mappings = key.mappings.match( + event = Event.keyLongDown, + inputMode = inputManager.mode, + packageName = currentInputEditorInfo.packageName, + alt = event.isAltPressed, + ctrl = event.isCtrlPressed + ) + + mappings?.map { mapping -> + if (mapping.command != null) { + inputManager.handler?.onRunCommand(mapping.command, key, event, keyDownStats) + } + + if (mapping.overrideConsume) consume = mapping.consume + } + + return when (consume) { + true -> true + false -> false + else -> super.onKeyLongPress(keyCode, event) + } } - override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) { - if (restarting) restartInput() - else startInput(attribute) + override fun onRecord(finishComposing: Boolean) { + if (finishComposing) finishComposingText() + if (!isInputViewShown) requestShowSelf(InputMethodManager.SHOW_IMPLICIT) - super.onStartInput(attribute, restarting) + record() + } + + override fun onReplaceText(text: String) { + currentInputConnection?.run { + beginBatchEdit() + setComposingRegion(0, text.length) + commitText(text, 1) + endBatchEdit() + } + } + + override fun onShowInputRequested(flags: Int, configChange: Boolean): Boolean { + return (inputManager.mode != InputMode.Number && inputManager.mode != InputMode.Idle) + } + + override fun onStartInput(editorInfo: EditorInfo?, restarting: Boolean) { + ioJob?.cancel() // Cancel possible transcription job + toast?.cancel() + + if (editorInfo == null) return + + inputManager.selectHandler(editorInfo) } override fun onUpdateSelection( @@ -230,7 +259,13 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene candidatesStart: Int, candidatesEnd: Int ) { - cursorPosition = newSelEnd + inputManager.handler?.run { + if (candidatesStart != candidatesEnd) { + onComposeText(lastComposedText, candidatesStart, candidatesEnd) + } + + onUpdateCursorPosition(newSelEnd) + } super.onUpdateSelection( oldSelStart, @@ -242,412 +277,166 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene ) } - override fun onGetSuggestions(p0: Array?) { - TODO("Not yet implemented") - } - - override fun onGetSentenceSuggestions(suggestionsInfo: Array?) { - clearCandidates() - - suggestionsInfo?.map { - val suggestions = it.getSuggestionsInfoAt(0) - - for (index in 0 until suggestions.suggestionsCount) { - val suggestion = suggestions.getSuggestionAt(index) - - candidates.add(suggestion) - } - - if (candidates.isNotEmpty()) loadCandidates() + override fun onStartIntent(intent: Intent) { + if (Settings.canDrawOverlays(this)) { + startActivity(intent) } } - private fun candidatesToLowerCase() { - candidates.forEachIndexed { index, candidate -> - candidates[index] = candidate.lowercase() - } + override fun onSwitchInputHandler(inputMode: InputMode) { + inputManager.switchToHandler(inputMode) } - private fun candidatesToUpperCase() { - candidates.forEachIndexed { index, candidate -> - candidates[index] = candidate.uppercase() - } + override fun onTranscribe() { + transcribe() } - private fun capitalizeCandidates() { - candidates.forEachIndexed { index, candidate -> - candidates[index] = candidate.lowercase().replaceFirstChar { it.uppercase() } + override fun onTriggerKeyEvent(event: KeyEvent) { + currentInputConnection?.sendKeyEvent(event) + } + + override fun onUpdateStatusIcon(icon: Int?) { + if (icon == null) hideStatusIcon() + else showStatusIcon(icon) + } + + private fun cancelCompose() { + if (!composing) return + + currentInputConnection?.let { + it.beginBatchEdit() + it.setComposingText("", 1) + it.finishComposingText() + it.endBatchEdit() } } private fun clearCandidates() { - clearCandidateUI() - - candidates.clear() - candidateIndex = 0 - } - - private fun clearCandidateUI() { val candidatesView = inputView?.findViewById(R.id.suggestions) ?: return candidatesView.removeAllViews() } - private fun commitText(text: CharSequence, start: Int, end: Int): Boolean { - return (markComposingRegion(start, end) && composeText(text, 1) && finishComposingText()) + private fun markComposingRegion(start: Int? = null, end: Int? = null) { + this.composing = currentInputConnection?.run { + val composeStart = start ?: cursorPosition + val composeEnd = end ?: cursorPosition + + setComposingRegion(composeStart, composeEnd) + } ?: false } - private fun composeText(text: CharSequence, cursorPosition: Int = 1): Boolean { - if (!composing) return false + private fun commitText(text: CharSequence) { + markComposingRegion() + composeText(text) + finishComposingText() + clearCandidates() + } - lastComposedString = text.toString() + private fun composeText(text: CharSequence, cursorPosition: Int = 1) { + if (!composing) return - return currentInputConnection?.setComposingText(text, cursorPosition) ?: false + currentInputConnection?.let { + if (it.setComposingText(text, cursorPosition)) lastComposedText = text + } } private fun deleteText(beforeCursor: Int, afterCursor: Int) { - currentInputConnection?.deleteSurroundingText(beforeCursor, afterCursor) - - updateInputStatus() - } - - private fun enableInputMode(mode: WKT9InputMode) { - if (mode != WKT9InputMode.FN) lastInputMode = mode - - inputMode = when(mode) { - WKT9InputMode.ALPHA -> alphaInputMode - WKT9InputMode.NUMERIC -> numericInputMode - WKT9InputMode.WORD -> wordInputMode - WKT9InputMode.FN -> fnInputMode - else -> idleInputMode + currentInputConnection?.run { + deleteSurroundingText(beforeCursor, afterCursor) } } - private fun enableTextInputMode(variation: Int, flags: Int) { - 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 fun finishComposingText() { + if (!composing) return - val mode: WKT9InputMode = if (letterVariations.contains(variation)) { - allowSuggestions = false + currentInputConnection?.let { + it.finishComposingText() - WKT9InputMode.ALPHA - } else if (lastInputMode == WKT9InputMode.ALPHA) { - allowSuggestions = flags != InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - - WKT9InputMode.ALPHA - } else { - allowSuggestions = flags != InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - - WKT9InputMode.WORD + inputManager.handler?.onCommitText() } - spellCheckerSession = if (allowSuggestions) { - val textServiceManager = getSystemService(TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager + composing = false + } - textServiceManager.newSpellCheckerSession(null, locale, this, false) - } else { - spellCheckerSession?.apply { - cancel() - close() - } + private fun getCandidateIndex(candidate: View): Int? { + val candidatesView = inputView?.findViewById(R.id.suggestions) ?: return null - null + for (i in 0 until candidatesView.childCount) { + val child: View = candidatesView.getChildAt(i) + + if (candidate == child) return i } - enableInputMode(mode) + return null } - private fun finishComposingText(): Boolean { - return if (composing) { - composing = false - - if (allowSuggestions && inputMode is AlphaInputMode) handleSuggestions() - - updateInputStatus() - - currentInputConnection?.finishComposingText() ?: false - } else false - } - - private fun handleSuggestions() { - val lastComposed = lastComposedString ?: return - val lastChar = lastComposed.lowercase().last() - val code = keypad.codeForLetter(lastChar) - - if (lastComposed.length != 1 || code == null) { - commitHistory.clear() - - return - } - - commitHistory.add(lastComposed) - -// loadSuggestions(commitHistory.joinToString("")) - } - - private fun loadSuggestions(word: String) { - val info = arrayOf(TextInfo(word.plus("#"), 0, word.length + 1, 0, 0)) - - spellCheckerSession?.getSentenceSuggestions(info, 10) - } - - @SuppressLint("DiscouragedApi") - private fun getIconResource(): Int { - val mode = inputMode?.mode ?: return resources.getIdentifier("wkt9", "drawable", packageName) - val name = mode.plus("_") - .plus(locale.toString()) - .plus("_") - .plus(inputStatus.toString()) - .replace('-', '_') - .lowercase() - - return resources.getIdentifier(name, "drawable", packageName) - } - - private fun goHome() { - if (Settings.canDrawOverlays(this)) { - val startMain = Intent(Intent.ACTION_MAIN) - - startMain.addCategory(Intent.CATEGORY_HOME) - startMain.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - startActivity(startMain) - } else { - val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) - - intent.data = Uri.parse("package:$packageName") - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - startActivity(intent) - } - } - - private fun handleComposeTimeout(timeout: Int?) { - this.timeout = timeout - - commitJob?.cancel() - - if (timeout == null) return - - commitJob = commitScope.launch { - delay(timeout.toLong()) - finishComposingText() - clearCandidates() - } - } - - private fun handleKeyEventResult(res: KeyEventResult): Boolean { - if (res.finishComposing) finishComposingText() - if (res.startComposing) markComposingRegion() - if (res.increaseWeight) onIncreaseWeight() - if (!res.codeWord.isNullOrEmpty()) onCodeWordUpdate(res.codeWord, res.timeout) - if (!res.candidates.isNullOrEmpty()) onCandidates(res.candidates, res.timeout) - if (!res.commit.isNullOrEmpty()) onCommit(res.commit) - if (res.deleteBeforeCursor > 0 || res.deleteAfterCursor > 0) onDelete(res.deleteBeforeCursor, res.deleteAfterCursor) - if (res.goHome) goHome() - if (res.left) onLeft() - if (res.right) onRight() - if (res.record) onRecord() - if (res.transcribe) onTranscribe() - if (res.updateInputStatus) updateInputStatus() - if (res.updateWordStatus) onUpdateWordStatus() - if (res.focus) onFocus() - if (res.switchInputMode != null) onSwitchInputMode(res.switchInputMode) - if (res.toggleFunctionMode) onToggleFunctionMode() - if (res.increaseVolume) onIncreaseVolume() - if (res.decreaseVolume) onDecreaseVolume() - if (res.increaseBrightness) onIncreaseBrightness() - if (res.decreaseBrightness) onDecreaseBrightness() - if (res.keyEvent != null) onKeyEvent(res.keyEvent) - - return res.consumed - } - - private fun isSentenceStart(): Boolean { - if (cursorPosition == 0) return true - - val textBeforeCursor = currentInputConnection?.getTextBeforeCursor(10, 0) ?: return false - - if ( - textBeforeCursor.trimEnd().isEmpty() || - listOf('.', '!', '?').contains(textBeforeCursor.trimEnd().last())) return true - - return false - } - - private fun loadCandidates() { + private fun loadCandidates( + candidates: List, + current: Int?, + timeout: Long?, + start: Int?, + end: Int?, + notifyOnChange: Boolean = false + ) { val candidatesView = inputView?.findViewById(R.id.suggestions) ?: return + clearCandidates() + commitJob?.cancel() + candidates.forEachIndexed { index, candidate -> - val layout = if (index == candidateIndex) R.layout.current_suggestion else R.layout.suggestion + val layout = if (index == current) R.layout.current_suggestion else R.layout.suggestion val candidateView = layoutInflater.inflate(layout, null) val textView = candidateView.findViewById(R.id.suggestion_text) textView.text = candidate + candidateView.setOnClickListener { view -> + getCandidateIndex(view)?.let { index -> + loadCandidates(candidates, index, timeout, start, end, notifyOnChange) + } + } + + candidateView.setOnLongClickListener { + val view = it as LinearLayout + val suggestion = view.findViewById(R.id.suggestion_text) + val text = suggestion.text + + inputManager.handler?.onLongClickCandidate(text.toString()) + + true + } + + if (index == current) { + if (!composing) markComposingRegion(start, end) + + composeText(candidate) + + if (notifyOnChange) { + inputManager.handler?.onCandidateSelected(candidate) + } + } + candidatesView.addView(candidateView) } - } - private fun markComposingRegion(start: Int? = null, end: Int? = null): Boolean { - if (composing) return false + if (!isInputViewShown) requestShowSelf(InputMethodManager.SHOW_IMPLICIT) + if (timeout == null) return - val composeStart = start ?: cursorPosition - val composeEnd = end ?: cursorPosition - - composing = currentInputConnection?.setComposingRegion(composeStart, composeEnd) ?: false - - return composing - } - - private fun onCandidates(candidates: List, timeout: Int?) { - clearCandidates() - - candidates.forEach { - val candidate = - when (inputStatus) { - Status.CAP -> it.replaceFirstChar { char -> char.uppercase() } - Status.UPPER -> it.uppercase() - else -> it - } - - this.candidates.add(candidate) + commitJob = commitScope.launch { + delay(timeout.toLong()) + resetKeyStats() + finishComposingText() } - - loadCandidates() - composeText(this.candidates[candidateIndex]) - handleComposeTimeout(timeout) - } - - private fun onCodeWordUpdate(codeWord: StringBuilder, timeout: Int?) { - clearCandidates() - - queryJob?.cancel() - queryJob = queryScope.launch { - val hasCandidates = queryT9Candidates(codeWord, 10) - - if (!hasCandidates) return@launch - - loadCandidates() - composeText(candidates[candidateIndex], 1) - handleComposeTimeout(timeout) - } - } - - private fun onCommit(text: String) { - commitText(text, cursorPosition, cursorPosition) - } - - private fun onDecreaseBrightness() { - if (!Settings.System.canWrite(this)) requestWriteSettings() - else { - var brightness = Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS) - - brightness -= 5 - - if (brightness < 0) brightness = 0 - - Settings.System.putInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS, brightness) - } - } - - private fun onDecreaseVolume() { - val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - - audioManager.adjustVolume(AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI) - } - - private fun onDelete(beforeCursor: Int, afterCursor: Int) { - val newCursorPosition = cursorPosition - beforeCursor - - clearCandidates() - deleteText(beforeCursor, afterCursor) - - if (newCursorPosition < 1) return - - val extractedTextRequest = ExtractedTextRequest() - val request = currentInputConnection?.getExtractedText(extractedTextRequest, 0) - val text = request?.text - - text?.let { - Log.d(tag, "Last char before cursor = ${it[newCursorPosition - 1]}") - } - -// Log.d(tag, "Text: $sub, ${text?.length}, $cursorPosition") - } - - private fun onFocus() { - requestShowSelf(InputMethodManager.SHOW_IMPLICIT) - } - - private fun onKeyEvent(keyEvent: KeyEvent) { - currentInputConnection?.sendKeyEvent(keyEvent) - } - - private fun onToggleFunctionMode() { - if (inputMode is FNInputMode) enableInputMode(lastInputMode) - else enableInputMode(WKT9InputMode.FN) - - updateInputStatus() - } - - private fun onIncreaseBrightness() { - if (!Settings.System.canWrite(this)) requestWriteSettings() - else { - var brightness = Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS) - - brightness += 5 - - if (brightness > 255) brightness = 255 - - Settings.System.putInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS, brightness) - } - } - - private fun onIncreaseVolume() { - val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - - audioManager.adjustVolume(AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI) - } - - private fun onIncreaseWeight() { - val word = commitHistory.last() - - if (word.isEmpty()) return - - queryScope.launch { - wordDao.increaseWeight(word) - } - } - - private fun onLeft() { - if (candidates.isEmpty()) return - - candidateIndex-- - - if (candidateIndex < 0) candidateIndex = candidates.count() - 1 - - clearCandidateUI() - loadCandidates() - composeText(candidates[candidateIndex]) - handleComposeTimeout(this.timeout) } @Suppress("DEPRECATION") - private fun onRecord() { + private fun record() { // The recorder must be busy... - if (recorder !== null || !isInputViewShown) return + if (recorder !== null) return clearCandidates() + requestShowSelf(InputMethodManager.SHOW_IMPLICIT) // Delete possible existing recording recording?.delete() @@ -679,31 +468,14 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene } } - private fun onRight() { - if (candidates.isEmpty()) return - - candidateIndex++ - - if (candidateIndex >= candidates.count()) candidateIndex = 0 - - clearCandidateUI() - loadCandidates() - composeText(candidates[candidateIndex]) - handleComposeTimeout(this.timeout) + private fun resetKeyStats() { + keyDownStats.keyCode = 0 + keyDownStats.repeats = 0 + keyUpStats.keyCode = 0 + keyUpStats.repeats = 0 } - private fun onSwitchInputMode(mode: WKT9InputMode) { - when (mode) { - WKT9InputMode.ALPHA -> enableInputMode(WKT9InputMode.ALPHA) - WKT9InputMode.NUMERIC -> enableInputMode(WKT9InputMode.NUMERIC) - else -> enableInputMode(WKT9InputMode.WORD) - } - - clearCandidates() - updateInputStatus() - } - - private fun onTranscribe() { + private fun transcribe() { val recorder = this.recorder ?: return recorder.stop() @@ -725,107 +497,10 @@ class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListene try { val transcription = whisper.run(recording!!) - commitText(transcription.plus(" "), cursorPosition, cursorPosition) + commitText(transcription.plus(' ')) } catch (e: IOException) { Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e) } } } - - private fun onUpdateWordStatus() { - clearCandidateUI() - - when (inputStatus) { - Status.CAP -> { - inputStatus = Status.UPPER - - candidatesToUpperCase() - } - - Status.UPPER -> { - inputStatus = Status.LOWER - - candidatesToLowerCase() - } - - else -> { - inputStatus = Status.CAP - - capitalizeCandidates() - } - } - - showStatusIcon(getIconResource()) - loadCandidates() - composeText(candidates[candidateIndex]) - } - - private fun restartInput() { - inputMode?.restart() - - clearCandidates() - } - - private fun requestWriteSettings() { - val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) - - intent.data = Uri.parse("package:$packageName") - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - startActivity(intent) - } - - private fun startInput(attribute: EditorInfo?) { - val inputType = attribute?.inputType - val inputClass = inputType?.and(InputType.TYPE_MASK_CLASS) ?: 0 - val typeVariation = inputType?.and(InputType.TYPE_MASK_VARIATION) ?: 0 - val typeFlags = inputType?.and(InputType.TYPE_MASK_FLAGS) ?: 0 - - cursorPosition = attribute?.initialSelEnd ?: 0 - - val forceNumeric = resources.getStringArray(R.array.input_mode_numeric) - - if (forceNumeric.contains(attribute?.packageName)) enableInputMode(WKT9InputMode.NUMERIC) - else { - when (inputClass) { - InputType.TYPE_CLASS_DATETIME, - InputType.TYPE_CLASS_NUMBER, - InputType.TYPE_CLASS_PHONE -> enableInputMode(WKT9InputMode.NUMERIC) - - InputType.TYPE_CLASS_TEXT -> enableTextInputMode(typeVariation, typeFlags) - else -> enableInputMode(WKT9InputMode.IDLE) - } - } - - attribute?.packageName?.let { - inputMode?.packageName(it) - } - - updateInputStatus() - } - - private fun updateInputStatus() { - inputStatus = inputMode?.status ?: Status.CAP - - if (inputStatus == Status.CAP && !isSentenceStart()) inputStatus = Status.LOWER - - showStatusIcon(getIconResource()) - } - - private suspend fun queryT9Candidates(codeWord: StringBuilder, limit: Int = 10): Boolean { - val words = wordDao.findCandidates(codeWord.toString(), limit) - - words.forEach { word -> - val candidate = - when (inputStatus) { - Status.CAP -> word.word.replaceFirstChar { it.uppercase() } - Status.UPPER -> word.word.uppercase() - else -> word.word - } - - candidates.add(candidate) - } - - return words.isNotEmpty() - } } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/WKT9Interface.kt b/app/src/main/java/net/mezimmah/wkt9/WKT9Interface.kt new file mode 100644 index 0000000..3c1c9fe --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/WKT9Interface.kt @@ -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, + 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?) +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/dao/WordDao.kt b/app/src/main/java/net/mezimmah/wkt9/dao/WordDao.kt index cc33db0..d38551d 100644 --- a/app/src/main/java/net/mezimmah/wkt9/dao/WordDao.kt +++ b/app/src/main/java/net/mezimmah/wkt9/dao/WordDao.kt @@ -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 + @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") diff --git a/app/src/main/java/net/mezimmah/wkt9/entity/Word.kt b/app/src/main/java/net/mezimmah/wkt9/entity/Word.kt index fabc046..42a1fe0 100644 --- a/app/src/main/java/net/mezimmah/wkt9/entity/Word.kt +++ b/app/src/main/java/net/mezimmah/wkt9/entity/Word.kt @@ -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, diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt deleted file mode 100644 index 750efd6..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt +++ /dev/null @@ -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 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/BaseInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/BaseInputMode.kt deleted file mode 100644 index 370c03c..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/BaseInputMode.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/Capitalize.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/Capitalize.kt new file mode 100644 index 0000000..9fdaa02 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/Capitalize.kt @@ -0,0 +1,50 @@ +package net.mezimmah.wkt9.inputmode + +import android.text.InputType + +class Capitalize( + private val capMode: Int?, + private val sentenceDelimiters: List = 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/CursorPositionInfo.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/CursorPositionInfo.kt new file mode 100644 index 0000000..b203c70 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/CursorPositionInfo.kt @@ -0,0 +1,6 @@ +package net.mezimmah.wkt9.inputmode + +data class CursorPositionInfo( + val startWord: Boolean, + val startSentence: Boolean +) diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/FNInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/FNInputMode.kt deleted file mode 100644 index 2f17f7f..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/FNInputMode.kt +++ /dev/null @@ -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 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputHandler.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputHandler.kt new file mode 100644 index 0000000..dcf4fdb --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputHandler.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputMode.kt deleted file mode 100644 index 8b2ded8..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/IdleInputMode.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandler.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandler.kt new file mode 100644 index 0000000..48888b9 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandler.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandlerInterface.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandlerInterface.kt new file mode 100644 index 0000000..67deee7 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputHandlerInterface.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputManager.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputManager.kt new file mode 100644 index 0000000..48e9d67 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputManager.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt index aa4b74b..1f1f3d4 100644 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/LetterInputHandler.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/LetterInputHandler.kt new file mode 100644 index 0000000..6292cce --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/LetterInputHandler.kt @@ -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?) { + TODO("Not yet implemented") + } + + override fun onGetSentenceSuggestions(results: Array?) { + val candidates = mutableListOf() + + 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() + + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/NumberInputHandler.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/NumberInputHandler.kt new file mode 100644 index 0000000..40623fe --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/NumberInputHandler.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt deleted file mode 100644 index 30f6ee4..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt +++ /dev/null @@ -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 = " " - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt deleted file mode 100644 index 9c5c9de..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mezimmah.wkt9.inputmode - -enum class Status { - CAP, - UPPER, - LOWER, - NUM, - NA -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/WKT9InputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/WKT9InputMode.kt deleted file mode 100644 index 1adc987..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/WKT9InputMode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mezimmah.wkt9.inputmode - -enum class WKT9InputMode { - WORD, - ALPHA, - NUMERIC, - IDLE, - FN -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputHandler.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputHandler.kt new file mode 100644 index 0000000..da3de16 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputHandler.kt @@ -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 { + val results = wordDao.findCandidates(codeWord.toString(), limit) + val capitalize = Capitalize(capMode) + val candidates = mutableListOf() + + results.forEach { result -> + candidates.add(capitalize.word(result.word, sentenceStart)) + } + + return candidates + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt deleted file mode 100644 index 2f21183..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt +++ /dev/null @@ -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 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/Command.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/Command.kt index 935eb65..740e110 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/Command.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/Command.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/CommandMapping.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/CommandMapping.kt new file mode 100644 index 0000000..d0a77a6 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/CommandMapping.kt @@ -0,0 +1,15 @@ +package net.mezimmah.wkt9.keypad + +import net.mezimmah.wkt9.inputmode.InputMode + +data class CommandMapping( + val events: List? = null, + val inputModes: List? = null, + val packageNames: List? = 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, +) \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/Event.java b/app/src/main/java/net/mezimmah/wkt9/keypad/Event.java new file mode 100644 index 0000000..63644a1 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/Event.java @@ -0,0 +1,9 @@ +package net.mezimmah.wkt9.keypad; + +public enum Event { + keyDown, + keyLongDown, + keyDownRepeat, + afterShortDown, + afterLongDown +} diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/Key.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/Key.kt index 2800644..9fa2483 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/Key.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/Key.kt @@ -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] + } } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCodeMapping.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCodeMapping.kt deleted file mode 100644 index 7a520ca..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCodeMapping.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.mezimmah.wkt9.keypad - -import java.util.Properties - -class KeyCodeMapping( - private val keyMap: Map, -) { - - 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() - - 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt deleted file mode 100644 index 2ce27f2..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt +++ /dev/null @@ -1,89 +0,0 @@ -package net.mezimmah.wkt9.keypad - -class KeyCommandResolver ( - private val onShort: HashMap = HashMap(mapOf()), - private val onLong: HashMap = HashMap(mapOf()), - private val afterShort: HashMap = HashMap(mapOf()), - private val afterLong: HashMap = HashMap(mapOf()), - private val onRepeat: HashMap = 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, - )) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt deleted file mode 100644 index 1d987bd..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt +++ /dev/null @@ -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? = 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 -) diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventStat.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventStat.kt new file mode 100644 index 0000000..adc6f13 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventStat.kt @@ -0,0 +1,6 @@ +package net.mezimmah.wkt9.keypad + +data class KeyEventStat( + var keyCode: Int, + var repeats: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyLayout.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyLayout.kt index e52abb9..ab33357 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyLayout.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyLayout.kt @@ -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('*','#','\'','"','.','?','!',',','-','@','$','/','%',':','(',')') } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/Keypad.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/Keypad.kt index fbbeb08..9e1e0ad 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/Keypad.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/Keypad.kt @@ -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>, numericLayout: Map ) { - private val tag = "WKT9" private val letterCodeMap: MutableMap = 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] } } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/Mappings.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/Mappings.kt new file mode 100644 index 0000000..95564d0 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/Mappings.kt @@ -0,0 +1,40 @@ +package net.mezimmah.wkt9.keypad + +import net.mezimmah.wkt9.inputmode.InputMode + +class Mappings(private val mappings: List) { + fun match( + event: Event, + inputMode: InputMode, + packageName: String, + alt: Boolean = false, + ctrl: Boolean = false, + repeatCount: Int = 0, + ): MutableList? { + val commands = mutableListOf() + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt index d733eda..3978fdf 100644 --- a/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt +++ b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt @@ -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 -> // 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(key)?.isChecked = false + findPreference(this.key!!)?.isChecked = false } } @@ -32,6 +33,14 @@ class PreferencesFragment: PreferenceFragmentCompat(), super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + + findPreference(getString(R.string.overlay_key))?.isChecked = Settings.canDrawOverlays(context) + } + + override fun onStart() { + super.onStart() + + findPreference(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(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) + } } } } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/t9/T9.kt b/app/src/main/java/net/mezimmah/wkt9/t9/T9.kt index deba924..52ce569 100644 --- a/app/src/main/java/net/mezimmah/wkt9/t9/T9.kt +++ b/app/src/main/java/net/mezimmah/wkt9/t9/T9.kt @@ -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!") + } } } } diff --git a/app/src/main/java/net/mezimmah/wkt9/ui/Suggestions.kt b/app/src/main/java/net/mezimmah/wkt9/ui/Suggestions.kt deleted file mode 100644 index 87373f4..0000000 --- a/app/src/main/java/net/mezimmah/wkt9/ui/Suggestions.kt +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/app/src/main/res/layout/current_suggestion.xml b/app/src/main/res/layout/current_suggestion.xml index e5b8393..0006ae0 100644 --- a/app/src/main/res/layout/current_suggestion.xml +++ b/app/src/main/res/layout/current_suggestion.xml @@ -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" diff --git a/app/src/main/res/layout/suggestion.xml b/app/src/main/res/layout/suggestion.xml index ad6484d..cbf5b32 100644 --- a/app/src/main/res/layout/suggestion.xml +++ b/app/src/main/res/layout/suggestion.xml @@ -9,12 +9,11 @@ + android:textFontWeight="400" /> \ No newline at end of file diff --git a/app/src/main/res/layout/suggestions.xml b/app/src/main/res/layout/suggestions.xml index baf68b2..371d499 100644 --- a/app/src/main/res/layout/suggestions.xml +++ b/app/src/main/res/layout/suggestions.xml @@ -1,5 +1,5 @@ - - + android:layout_height="44dp" + android:orientation="horizontal" /> - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/symbols.xml b/app/src/main/res/layout/symbols.xml new file mode 100644 index 0000000..3411258 --- /dev/null +++ b/app/src/main/res/layout/symbols.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4bc94a3..8ac4383 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,11 +5,11 @@ #FF3700B3 #FF03DAC5 #FF018786 - #FF000000 + #CC000000 #FFFFFFFF - #637783 + #CC336699 #FFFCF0 - #C1E8FF - #FFE3C1 - #424242 + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b310fb3..78a108d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,17 +3,27 @@ WKT9 Preferences - Speech to Text + + Speech to Text + Speech to Text - speech_to_text - Enable Speech to Text - 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. + speech_to_text + Enable Speech to Text + 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. - whisper_url - Whisper Server URL - Provide an URL to the Whisper server. + whisper_url + Whisper Server URL + Provide an URL to the Whisper server. + + Draw over other activities + Draw over activities + Grant WKT9 permission to draw over other applications org.linphone + + + com.android.camera2 + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5639099..0bc2dc7 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -3,17 +3,26 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + app:title="@string/speech_to_text_cat" /> + app:key="@string/speech_to_text_key" + app:title="@string/speech_to_text_title" + app:summary="@string/speech_to_text_summary" /> + 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" /> + + + + +