Compare commits

...

15 Commits

Author SHA1 Message Date
a6ccd6d703 Include whisper in letter mode too 2023-12-04 11:57:56 -05:00
4da4eabc19 Include whisper again 2023-12-04 11:56:42 -05:00
23cccf2455 Get sentence position of cursor 2023-12-04 05:48:46 -05:00
48a9a2e822 Commit current word if we're switching modes 2023-12-04 05:22:04 -05:00
0ec4f4b262 Get initial sentence position state 2023-12-04 05:19:38 -05:00
3b0ce64b09 We need to store the word if the word or sentence gets delimited using the numeric 1 key. 2023-12-03 20:20:37 -05:00
3a1f1a595c Adjust delete to handle repeat count 2023-12-03 20:10:11 -05:00
92a4ac541c Slowly getting stable 2023-12-03 19:58:54 -05:00
6c59ee4d82 We;re not using that 2023-12-01 16:43:47 -05:00
76fa2d11d0 Don't need that anymore 2023-12-01 16:43:17 -05:00
ea361c1586 Making progress 2023-12-01 16:42:18 -05:00
a7e17eda9d Making progress 2023-12-01 16:30:59 -05:00
96d0443892 First release? 2023-11-13 11:58:48 -05:00
590975d708 Making progress 2023-11-09 10:26:17 -05:00
9d5c6364f0 Refactored 2023-11-08 16:15:17 -05:00
86 changed files with 658903 additions and 2315 deletions

123
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

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

BIN
app/release/app-release.apk Normal file

Binary file not shown.

View File

@@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "net.mezimmah.wkt9",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}

View File

@@ -2,6 +2,10 @@
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -18,13 +22,16 @@
android:theme="@style/Theme.WKT9">
<service
android:name=".WKT9"
android:name=".WKT9IME"
android:label="@string/app_name"
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method" />
</service>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
package net.mezimmah.wkt9
import android.content.Intent
import android.view.KeyEvent
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
interface IME {
fun onTriggerKeyEvent(event: KeyEvent)
fun onStartIntent(intent: Intent)
fun onCandidates(
candidates: ArrayList<CharSequence>,
current: Int? = 0
)
fun onWords(words: List<Word>, capMode: Int?)
fun onNextWord()
fun onPreviousWord()
fun onWordSelected(word: Word)
fun onDeleteWord(word: Word)
fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0)
fun onCompose(text: CharSequence)
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false)
fun defaultView()
fun onSwitchInputHandler(inputMode: InputMode)
fun onUpdateStatusIcon(icon: Int?)
fun record()
fun transcribe()
}

View File

@@ -1,831 +0,0 @@
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.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.voice.Whisper
import okio.IOException
import java.io.File
import java.util.Locale
class WKT9: InputMethodService(), SpellCheckerSession.SpellCheckerSessionListener {
private val tag = "WKT9"
// Dao - Database
private lateinit var db: AppDatabase
private lateinit var wordDao: WordDao
private lateinit var settingDao: SettingDao
// 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<String> = 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<String> = 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
// 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
locale = inputMethodSubtype?.let {
Locale.forLanguageTag(it.languageTag)
} ?: Locale.forLanguageTag("en-US")
Log.d(tag, "WKT9 is loading: $locale")
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()
t9.initializeWords(languageTag)
super.onCreate()
}
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
Log.d(tag, "Here we are")
return InlineSuggestionsRequest.Builder(ArrayList<InlinePresentationSpec>())
.setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
.build()
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
inputView = layoutInflater.inflate(R.layout.suggestions, null)
return inputView
}
override fun onFinishInput() {
super.onFinishInput()
clearCandidates()
spellCheckerSession?.cancel()
spellCheckerSession?.close()
inputMode = null
cursorPosition = 0
inputStatus = Status.CAP
spellCheckerSession = null
}
override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput)
clearCandidates()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val key = keypad.getKey(keyCode) ?: return super.onKeyDown(keyCode, event)
val repeatCount = event?.repeatCount ?: 0
return inputMode?.let {
val keyEventResult =
if (repeatCount > 0) it.onKeyDownRepeatedly(key, repeatCount, composing)
else {
event?.startTracking()
it.onKeyDown(key, composing)
}
handleKeyEventResult(keyEventResult)
} ?: 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
return inputMode?.let {
val keyEventResult =
if (keyDownMS >= longPressTimeout) it.afterKeyLongDown(key, keyDownMS, composing)
else it.afterKeyDown(key, composing)
handleKeyEventResult(keyEventResult)
} ?: super.onKeyUp(keyCode, event)
}
override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
val key = keypad.getKey(keyCode) ?: return super.onKeyLongPress(keyCode, event)
return inputMode?.let {
val keyEventResult = it.onKeyLongDown(key, composing)
handleKeyEventResult(keyEventResult)
} ?: super.onKeyLongPress(keyCode, event)
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
if (restarting) restartInput()
else startInput(attribute)
super.onStartInput(attribute, restarting)
}
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {
cursorPosition = newSelEnd
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
}
override fun onGetSuggestions(p0: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onGetSentenceSuggestions(suggestionsInfo: Array<out SentenceSuggestionsInfo>?) {
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()
}
}
private fun candidatesToLowerCase() {
candidates.forEachIndexed { index, candidate ->
candidates[index] = candidate.lowercase()
}
}
private fun candidatesToUpperCase() {
candidates.forEachIndexed { index, candidate ->
candidates[index] = candidate.uppercase()
}
}
private fun capitalizeCandidates() {
candidates.forEachIndexed { index, candidate ->
candidates[index] = candidate.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun clearCandidates() {
clearCandidateUI()
candidates.clear()
candidateIndex = 0
}
private fun clearCandidateUI() {
val candidatesView = inputView?.findViewById<LinearLayout>(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 composeText(text: CharSequence, cursorPosition: Int = 1): Boolean {
if (!composing) return false
lastComposedString = text.toString()
return currentInputConnection?.setComposingText(text, cursorPosition) ?: false
}
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
}
}
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
)
val mode: WKT9InputMode = if (letterVariations.contains(variation)) {
allowSuggestions = false
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
}
spellCheckerSession = if (allowSuggestions) {
val textServiceManager = getSystemService(TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
textServiceManager.newSpellCheckerSession(null, locale, this, false)
} else {
spellCheckerSession?.apply {
cancel()
close()
}
null
}
enableInputMode(mode)
}
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() {
val candidatesView = inputView?.findViewById<LinearLayout>(R.id.suggestions) ?: return
candidates.forEachIndexed { index, candidate ->
val layout = if (index == candidateIndex) R.layout.current_suggestion else R.layout.suggestion
val candidateView = layoutInflater.inflate(layout, null)
val textView = candidateView.findViewById<TextView>(R.id.suggestion_text)
textView.text = candidate
candidatesView.addView(candidateView)
}
}
private fun markComposingRegion(start: Int? = null, end: Int? = null): Boolean {
if (composing) return false
val composeStart = start ?: cursorPosition
val composeEnd = end ?: cursorPosition
composing = currentInputConnection?.setComposingRegion(composeStart, composeEnd) ?: false
return composing
}
private fun onCandidates(candidates: List<String>, 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)
}
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() {
// The recorder must be busy...
if (recorder !== null || !isInputViewShown) return
clearCandidates()
// Delete possible existing recording
recording?.delete()
// Toast settings
val text = "Recording now.\nRelease the button to start transcribing."
val duration = Toast.LENGTH_SHORT
// Instantiate recorder and start recording
recorder = MediaRecorder().also {
recording = File.createTempFile("recording.3gp", null, cacheDir)
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
it.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
it.setOutputFile(recording)
try {
it.prepare()
it.start()
toast?.cancel()
toast = Toast.makeText(this, text, duration).apply {
this.show()
}
} catch (e: Exception) {
Log.d(tag, "Failed to start recording", e)
}
}
}
private fun onRight() {
if (candidates.isEmpty()) return
candidateIndex++
if (candidateIndex >= candidates.count()) candidateIndex = 0
clearCandidateUI()
loadCandidates()
composeText(candidates[candidateIndex])
handleComposeTimeout(this.timeout)
}
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() {
val recorder = this.recorder ?: return
recorder.stop()
recorder.reset()
recorder.release()
this.recorder = null
val text = "Sending recording to speech-to-text server for transcription."
val duration = Toast.LENGTH_SHORT
toast?.cancel()
toast = Toast.makeText(this, text, duration).apply {
this.show()
}
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = whisper.run(recording!!)
commitText(transcription.plus(" "), cursorPosition, cursorPosition)
} 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()
}
}

View File

@@ -0,0 +1,353 @@
package net.mezimmah.wkt9
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.inputmethodservice.InputMethodService
import android.provider.Settings
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodSubtype
import androidx.preference.PreferenceManager
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputhandler.IdleInputHandler
import net.mezimmah.wkt9.inputhandler.InputHandler
import net.mezimmah.wkt9.inputhandler.LetterInputHandler
import net.mezimmah.wkt9.inputhandler.NumberInputHandler
import net.mezimmah.wkt9.inputhandler.WordInputHandler
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.layout.LoadingLayout
import net.mezimmah.wkt9.layout.MessageLayout
import net.mezimmah.wkt9.layout.WordsLayout
import net.mezimmah.wkt9.t9.T9
import net.mezimmah.wkt9.voice.Whisper
import java.util.Locale
class WKT9IME: IME, InputMethodService() {
private val tag = "WKT9"
private val inputModeManager = InputModeManager(this)
private val whisper: Whisper = Whisper(this)
private lateinit var locale: Locale
private var inputHandler: InputHandler? = null
private var wordsLayoutView: WordsLayout? = null
private var loadingLayoutView: LoadingLayout? = null
private var messageLayoutView: MessageLayout? = null
private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0)
private var composing: Boolean = false
private var selectionStart: Int = 0
private var selectionEnd: Int = 0
private var capMode: Int? = null
override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) {
// this.candidates?.load(candidates, current)
}
override fun onCommit(text: CharSequence, beforeCursor: Int, afterCursor: Int) {
currentInputConnection?.run {
beginBatchEdit()
setComposingRegion(selectionEnd - beforeCursor, selectionEnd + afterCursor)
setComposingText(text, 1)
finishComposingText()
endBatchEdit()
}
}
override fun onCompose(text: CharSequence) {
currentInputConnection?.run {
if (!composing) setComposingRegion(selectionStart, selectionEnd)
setComposingText(text, 1)
}
}
@SuppressLint("InflateParams")
override fun onCreate() {
Log.d(tag, "Starting WKT9")
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val languageTag = inputMethodManager.currentInputMethodSubtype?.languageTag ?: "en-US"
locale = Locale.forLanguageTag(languageTag)
initializeDictionary(locale)
super.onCreate()
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
wordsLayoutView = layoutInflater.inflate(R.layout.words, null) as WordsLayout
loadingLayoutView = layoutInflater.inflate(R.layout.loading, null) as LoadingLayout
messageLayoutView = layoutInflater.inflate(R.layout.message, null) as MessageLayout
return wordsLayoutView
}
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
super.onCurrentInputMethodSubtypeChanged(newSubtype)
newSubtype?.let {
locale = Locale.forLanguageTag(it.languageTag)
initializeDictionary(locale)
inputHandler?.onSwitchLocale(locale)
}
}
override fun onDeleteText(beforeCursor: Int, afterCursor: Int, finishComposing: Boolean) {
deleteText(beforeCursor, afterCursor)
}
override fun onDeleteWord(word: Word) {
inputHandler?.onDeleteWord(word)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyDownStats.keyCode != keyCode) {
keyDownStats.keyCode = keyCode
keyDownStats.repeats = 0
} else keyDownStats.repeats++
val key = Key.fromKeyCode(keyCode)
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 = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputHandler?.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 {
if (keyUpStats.keyCode != keyCode) {
keyUpStats.keyCode = keyCode
keyUpStats.repeats = 0
} else keyUpStats.repeats++
val key = Key.fromKeyCode(keyCode)
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 >= 400L) Event.afterLongDown else Event.afterShortDown,
inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed,
repeatCount = event.repeatCount
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputHandler?.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 = Key.fromKeyCode(keyCode)
if (event == null || key == null) return super.onKeyLongPress(keyCode, event)
var consume = key.consume
val mappings = key.mappings.match(
event = Event.keyLongDown,
inputMode = inputModeManager.currentMode,
packageName = currentInputEditorInfo.packageName,
fn = event.isFunctionPressed,
ctrl = event.isCtrlPressed
)
mappings?.map { mapping ->
if (mapping.command != null) {
inputHandler?.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 onShowInputRequested(flags: Int, configChange: Boolean): Boolean {
return (
inputModeManager.currentMode != InputMode.Number &&
inputModeManager.currentMode != InputMode.Idle
)
}
override fun onStartInput(editorInfo: EditorInfo?, restarting: Boolean) {
val mode = inputModeManager.selectModeByEditor(editorInfo)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
switchInputMode(mode)
}
override fun onStartIntent(intent: Intent) {
if (Settings.canDrawOverlays(this)) {
startActivity(intent)
}
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo?.let {
selectionStart = it.selectionStart
selectionEnd = it.selectionEnd
composing = if (it.composingTextStart == -1) {
if (composing) finishComposing()
false
} else if (it.selectionEnd != (it.composingTextStart + it.composingText.length)) {
onCommit()
true
} else true
}
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
}
override fun onSwitchInputHandler(inputMode: InputMode) {
val mode = inputModeManager.switchToMode(inputMode)
switchInputMode(mode)
}
override fun onTriggerKeyEvent(event: KeyEvent) {
currentInputConnection?.sendKeyEvent(event)
}
override fun onUpdateStatusIcon(icon: Int?) {
if (icon == null) hideStatusIcon()
else showStatusIcon(icon)
}
override fun onWords(words: List<Word>, capMode: Int?) {
this.capMode = capMode
wordsLayoutView?.words = words
}
override fun onWordSelected(word: Word) {
val compose = when (capMode) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> word.word.uppercase()
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> word.word.replaceFirstChar { it.uppercase() }
else -> word.word
}
this.onCompose(compose)
this.inputHandler?.onWordSelected(word)
}
override fun onNextWord() {
wordsLayoutView?.next()
}
override fun onPreviousWord() {
wordsLayoutView?.previous()
}
private fun deleteText(beforeCursor: Int, afterCursor: Int) {
currentInputConnection?.run {
deleteSurroundingText(beforeCursor, afterCursor)
}
}
override fun record() {
setInputView(messageLayoutView)
whisper.record()
}
override fun transcribe() {
setInputView(loadingLayoutView)
whisper.transcribe()
}
override fun defaultView() {
setInputView(wordsLayoutView)
}
private fun finishComposing() {
wordsLayoutView?.clear()
inputHandler?.onFinishComposing()
}
private fun initializeDictionary(locale: Locale) {
val t9 = T9(this, locale)
t9.initializeWords()
}
private fun switchInputMode(mode: InputMode) {
inputHandler = when(mode) {
InputMode.Word -> WordInputHandler(this, this, locale)
InputMode.Letter -> {
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val timeout = prefs.getString(getString(R.string.compose_timeout), "700")
val composeTimeout = timeout?.toLong() ?: 700L
timeout?.let { LetterInputHandler(this, this, locale, composeTimeout) }
}
InputMode.Number -> NumberInputHandler(this, this)
else -> IdleInputHandler(this, this)
}
}
}

View File

@@ -1,26 +1,26 @@
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("DELETE FROM word WHERE id = :id")
fun delete(id: Int)
@Query("SELECT * FROM word")
fun getAll(): List<Word>
@Query("SELECT * FROM word WHERE code LIKE :code || '%' " +
@Query("SELECT * FROM word WHERE code LIKE :code || '%' AND locale = :locale " +
"ORDER BY length, weight DESC LIMIT :limit")
suspend fun findCandidates(code: String, limit: Int = 10): List<Word>
suspend fun findCandidates(locale: String, code: String, limit: Int = 10): List<Word>
@Query("UPDATE word SET weight = weight + 1 WHERE word=:word")
suspend fun increaseWeight(word: String)
@Query("UPDATE word SET weight = weight + 1 WHERE id=:id")
suspend fun increaseWeight(id: Int)
@Query("SELECT * FROM word WHERE word = :word AND locale = :locale COLLATE NOCASE")
suspend fun selectWord(word: String, locale: String): Word?
}

View File

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

View File

@@ -0,0 +1,113 @@
package net.mezimmah.wkt9.inputhandler
import android.text.InputType
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.Keypad
open class DefaultInputHandler(
private val wkt9: WKT9IME
) {
private val capModes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
null
)
private var currentCapMode: Int? = null
protected val punctuationMarks = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
protected val sentenceDelimiters = listOf('.', '?', '!')
protected val wordDelimiters = listOf('\t', '\n', ' ', ',', ':', ';')
protected val tag = "WKT9"
protected val keypad: Keypad = Keypad()
protected var wordStart: Boolean = false
protected var sentenceStart: Boolean = false
init {
setCursorPositionStatus()
wkt9.currentInputEditorInfo?.let {
val inputType = it.inputType
val typeFlags = inputType.and(InputType.TYPE_MASK_FLAGS)
if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) == InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
} else if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_WORDS) == InputType.TYPE_TEXT_FLAG_CAP_WORDS) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_WORDS
} else if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) == InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
}
}
}
protected fun effectiveCapMode(): Int? {
return when (currentCapMode) {
InputType.TYPE_TEXT_FLAG_CAP_WORDS -> {
if (wordStart) InputType.TYPE_TEXT_FLAG_CAP_WORDS
else null
}
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> {
if (sentenceStart) InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
else null
}
else -> currentCapMode
}
}
protected open fun toggleCapMode(key: Key) {
var index = capModes.indexOf(currentCapMode)
when (key) {
Key.B2 -> {
if (index == 0) index = capModes.count()
index--
}
else -> index++
}
currentCapMode = capModes[index % capModes.count()]
}
protected fun triggerKeyEvent(keyCode: Int) {
val down = KeyEvent(KeyEvent.ACTION_DOWN, keyCode)
val up = KeyEvent(KeyEvent.ACTION_UP, keyCode)
wkt9.onTriggerKeyEvent(down)
wkt9.onTriggerKeyEvent(up)
}
protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode)
}
protected fun setCursorPositionStatus() {
val textBeforeCursor = getTextBeforeCursor()
if (
textBeforeCursor.isNullOrEmpty() ||
textBeforeCursor.trim().isEmpty() ||
sentenceDelimiters.contains(textBeforeCursor.trim().last())
) {
sentenceStart = true
wordStart = true
} else if (wordDelimiters.contains(textBeforeCursor.last())) {
sentenceStart = false
wordStart = true
} else {
sentenceStart = false
wordStart = false
}
}
private fun getTextBeforeCursor(): CharSequence? {
return wkt9.currentInputConnection?.getTextBeforeCursor(15, 0)
}
}

View File

@@ -0,0 +1,52 @@
package net.mezimmah.wkt9.inputhandler
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import java.util.Locale
class IdleInputHandler(
val ime: IME,
val wkt9: WKT9IME,
) : DefaultInputHandler(wkt9), InputHandler {
init {
wkt9.showStatusIcon(R.drawable.idle_input)
}
override fun onFinishComposing() {}
override fun onDeleteWord(word: Word) {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.DIAL -> dial()
Command.CAMERA -> triggerKeyEvent(KeyEvent.KEYCODE_CAMERA)
// Command.NUMBER -> triggerOriginalKeyEvent(key)
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onSwitchLocale(locale: Locale) {
TODO("Not yet implemented")
}
override fun onWordSelected(word: Word) {}
private fun dial() {
val uri = "tel:"
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(uri)
// wkt9.onStartIntent(intent)
}
}

View File

@@ -0,0 +1,20 @@
package net.mezimmah.wkt9.inputhandler
import android.view.KeyEvent
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import java.util.Locale
interface InputHandler {
fun onDeleteWord(word: Word)
fun onFinishComposing()
fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat)
fun onSwitchLocale(locale: Locale)
fun onWordSelected(word: Word)
}

View File

@@ -0,0 +1,275 @@
package net.mezimmah.wkt9.inputhandler
import android.content.Context
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.textservice.SentenceSuggestionsInfo
import android.view.textservice.SpellCheckerSession
import android.view.textservice.SuggestionsInfo
import android.view.textservice.TextServicesManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.db.AppDatabase
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import java.util.Locale
class LetterInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
private var composeTimeout: Long,
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(wkt9), InputHandler {
private val db = AppDatabase.getInstance(wkt9)
private val wordDao = db.getWordDao()
private val word = StringBuilder()
private var lastKey: Key? = null
private var repeats: Int = 0
private var lastComposeChar: Char? = null
private var lastIcon: Int? = null
private var spellCheckerSession: SpellCheckerSession? = null
private var timeoutJob: Job? = null
init {
val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
spellCheckerSession = textServiceManager.newSpellCheckerSession(
null,
locale,
this,
false
)
updateIcon()
}
override fun onDeleteWord(word: Word) {}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
override fun onFinishComposing() {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete()
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> wkt9.record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onWordSelected(word: Word) {}
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
updateIcon()
}
private fun composeCharacter(key: Key) {
val composing = timeoutJob?.isActive ?: false
timeoutJob?.cancel()
if (lastKey == key) {
repeats++
word.deleteAt(word.length - 1)
} else {
resetKey(key)
if (composing) finishComposingChar()
}
val layout = KeyLayout.chars[key] ?: return
val index = repeats % layout.count()
val char = when(effectiveCapMode()) {
null -> layout[index].toString()
else -> layout[index].uppercase()
}
lastComposeChar = layout[index]
word.append(char)
wkt9.onCompose(char)
setComposeTimeout()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
timeoutJob?.cancel()
val index = stats.repeats % punctuationMarks.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
var beforeCursor = 0
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
wordStart = true
sentenceStart = index in 1..3
wkt9.onCommit(punctuationMarks[index], beforeCursor)
updateIcon()
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun resetKey(key: Key? = null) {
lastKey = key
repeats = 0
}
private fun storeWord() {
val str = word.toString()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
word.clear()
scope.launch {
val word = Word(
word = str,
code = keypad.getCodeForWord(str),
weight = 0,
length = str.length,
locale = locale.language
)
val result = wordDao.selectWord(str, locale.language)
if (result == null) wordDao.insert(word)
}
}
private fun finishComposingChar() {
wkt9.onCommit()
if (sentenceDelimiters.contains(lastComposeChar)) {
sentenceStart = true
wordStart = true
} else if (wordDelimiters.contains(lastComposeChar)) {
sentenceStart = false
wordStart = true
} else {
wordStart = false
sentenceStart = false
}
if (wordStart) {
word.deleteAt(word.length - 1)
if (word.isNotEmpty()) storeWord()
}
updateIcon()
}
private fun setComposeTimeout() {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
timeoutJob = scope.launch {
delay(composeTimeout)
resetKey()
finishComposingChar()
}
}
private fun delete() {
if (word.isNotEmpty()) word.deleteAt(word.length - 1)
resetKey()
if (timeoutJob?.isActive == true) {
timeoutJob?.cancel()
wkt9.onCompose("")
} else {
wkt9.onDeleteText(1)
}
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Word)
else wkt9.onSwitchInputHandler(InputMode.Number)
}
private fun updateIcon() {
val icon = when (effectiveCapMode()) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.letter_upper
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> R.drawable.letter_cap
else -> R.drawable.letter_lower
}
if (icon == lastIcon) return
wkt9.onUpdateStatusIcon(icon)
lastIcon = icon
}
}
//
// override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
// results?.map {
// val suggestions = it.getSuggestionsInfoAt(0)
//
// for (index in 0 until suggestions.suggestionsCount) {
// val suggestion = suggestions.getSuggestionAt(index)
//
// candidates.add(suggestion)
// }
// }
//
// if (candidates.isEmpty()) return
//
// candidateSource = CandidateSource.Dictionary
//
// wkt9.onCandidates(
// candidates = candidates,
// current = null
// )
// }
// private fun getSuggestions() {
// val lastWord = getLastWord()
//
// if (lastWord.length < 3) return
//
// val words = arrayOf(
// TextInfo(
// lastWord.plus("#"), // Add hash to string to get magic performance
// 0,
// lastWord.length + 1, // We added the hash, remember
// 0,
// 0
// )
// )
//
// spellCheckerSession?.getSentenceSuggestions(words, 15)
// }

View File

@@ -0,0 +1,42 @@
package net.mezimmah.wkt9.inputhandler
import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import java.util.Locale
class NumberInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
) : InputHandler {
private val tag = "WKT9"
init {
wkt9.showStatusIcon(R.drawable.number_input)
}
// We don't need to implement methods below
override fun onFinishComposing() {}
override fun onDeleteWord(word: Word) {}
override fun onSwitchLocale(locale: Locale) {}
override fun onWordSelected(word: Word) {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.INPUT_MODE -> inputMode(key)
else -> Log.d(tag, "Command not implemented: $command")
}
}
private fun inputMode(key: Key) {
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Letter)
else wkt9.onSwitchInputHandler(InputMode.Word)
}
}

View File

@@ -0,0 +1,208 @@
package net.mezimmah.wkt9.inputhandler
import android.annotation.SuppressLint
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.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.db.AppDatabase
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import java.util.Locale
class WordInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale
) : DefaultInputHandler(wkt9), InputHandler {
private val codeword = StringBuilder()
private var staleCodeword = false
private var lastSelectedWord: Word? = null
private val db = AppDatabase.getInstance(wkt9)
private val wordDao = db.getWordDao()
private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var queryJob: Job? = null
init {
updateIcon()
}
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
updateIcon()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
val index = stats.repeats % ends.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null
var beforeCursor = 0
if (lastIndex != null) beforeCursor += ends[lastIndex].length
wkt9.onCommit(ends[index], beforeCursor)
sentenceStart = index in 1..3
updateIcon()
}
override fun onDeleteWord(word: Word) {
ioScope.launch {
wordDao.delete(word.id)
handleCodewordChange(codeword)
}
}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
updateIcon()
}
override fun onFinishComposing() {
queryJob?.cancel()
codeword.clear()
increaseWordWeight()
staleCodeword = false
lastSelectedWord = null
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete(event.repeatCount)
Command.ENTER -> enter(key)
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
private fun record() {
if (codeword.isNotEmpty()) {
wkt9.onCommit()
codeword.clear()
}
wkt9.record()
}
private fun transcribe() {
wkt9.transcribe()
}
override fun onWordSelected(word: Word) {
lastSelectedWord = word
}
private fun buildCodeword(key: Key) {
// Don't build fruitless codeword
if (staleCodeword) return
val code = KeyLayout.numeric[key]
codeword.append(code)
queryJob?.cancel()
handleCodewordChange(codeword)
}
private fun delete(repeatCount: Int) {
lastSelectedWord = null
if (codeword.length > 1) {
codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword)
} else if (codeword.isNotEmpty() && repeatCount > 1) {
codeword.clear()
wkt9.onCompose("")
} else if (codeword.isNotEmpty()) {
codeword.clear()
wkt9.onCompose("")
} else {
wkt9.onDeleteText(1)
}
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun enter(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit("")
else triggerOriginalKeyEvent(key)
}
private fun handleCodewordChange(codeword: StringBuilder) {
queryJob?.cancel()
queryJob = queryScope.launch(Dispatchers.Main) {
val words = wordDao.findCandidates(locale.language, codeword.toString(), 10)
// The codeword is stale when it does not yield any candidates in the DB
staleCodeword = words.isEmpty()
if (words.isNotEmpty()) wkt9.onWords(words, effectiveCapMode())
}
}
private fun increaseWordWeight() {
val lastWord = lastSelectedWord ?: return
queryScope.launch {
wordDao.increaseWeight(lastWord.id)
}
}
private fun inputMode(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit()
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number)
else wkt9.onSwitchInputHandler(InputMode.Letter)
}
private fun moveCursor(key: Key) {
if (codeword.isEmpty()) triggerOriginalKeyEvent(key)
else if (key == Key.RIGHT) wkt9.onNextWord()
else if (key == Key.LEFT) wkt9.onPreviousWord()
}
@SuppressLint("DiscouragedApi")
private fun updateIcon() {
val mode = when (effectiveCapMode()) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper"
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> "cap"
else -> "lower"
}
val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
val icon = wkt9.resources.getIdentifier(name, "drawable", wkt9.packageName)
wkt9.onUpdateStatusIcon(icon)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.letter_cap),
Number(R.drawable.number_input),
Idle(R.drawable.wkt9)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,17 @@
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,
ENTER,
FINISH_DELETE,
INPUT_MODE,
MOVE_CURSOR,
NUMBER,
RECORD,
TRANSCRIBE,
BACK,
HOME,
FN
SPACE,
TRANSCRIBE
}

View File

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

View File

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

View File

@@ -1,24 +1,461 @@
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
) {
FN(KeyEvent.KEYCODE_FUNCTION, consume = false, Mappings(
listOf()
)),
CTRL_RIGHT(KeyEvent.KEYCODE_CTRL_RIGHT, consume = false, Mappings(
listOf()
)),
B1(KeyEvent.KEYCODE_BUTTON_1, consume = null, Mappings(
listOf(
CommandMapping(
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.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.SPACE
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N1(KeyEvent.KEYCODE_1, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
inputModes = listOf(InputMode.Number, InputMode.Idle),
overrideConsume = true,
consume = null
)
)
)),
N2(KeyEvent.KEYCODE_2, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N3(KeyEvent.KEYCODE_3, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N4(KeyEvent.KEYCODE_4, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N5(KeyEvent.KEYCODE_5, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N6(KeyEvent.KEYCODE_6, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N7(KeyEvent.KEYCODE_7, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N8(KeyEvent.KEYCODE_8, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
N9(KeyEvent.KEYCODE_9, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.CHARACTER
),
CommandMapping(
events = listOf(Event.keyLongDown),
inputModes = listOf(InputMode.Letter),
command = Command.NUMBER
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Word),
fn = true,
command = Command.NUMBER
),
CommandMapping(
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),
command = Command.DELETE
),
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Word, InputMode.Letter),
command = Command.FINISH_DELETE
),
CommandMapping(
inputModes = listOf(InputMode.Number),
overrideConsume = true,
consume = null
)
)
)),
UP(KeyEvent.KEYCODE_DPAD_UP, consume = null, Mappings(
listOf()
)),
DOWN(KeyEvent.KEYCODE_DPAD_DOWN, consume = null, Mappings(
listOf()
)),
LEFT(KeyEvent.KEYCODE_DPAD_LEFT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word),
command = Command.MOVE_CURSOR,
overrideConsume = true,
consume = true
)
)
)),
RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT, consume = null, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Word),
command = Command.MOVE_CURSOR,
overrideConsume = true,
consume = true
)
)
)),
ENTER(KeyEvent.KEYCODE_ENTER, consume = true, Mappings(
listOf(
CommandMapping(
events = listOf(Event.keyDown, Event.keyDownRepeat),
inputModes = listOf(InputMode.Letter, InputMode.Word),
command = Command.ENTER,
overrideConsume = true,
consume = true
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Idle),
packageNames = listOf("com.android.camera2"),
command = Command.CAMERA
)
)
));
companion object {
private val map = Key.values().associateBy(Key::keyCode)
fun fromKeyCode(keyCode: Int) = map[keyCode]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class LoadingLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class MessageLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,13 @@
package net.mezimmah.wkt9.voice
import android.media.MediaRecorder
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.WKT9IME
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
@@ -9,15 +17,74 @@ import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
class Whisper {
class Whisper(
private val wkt9: WKT9IME,
) {
private val tag = "WKT9"
private var ioJob: Job? = null
private var recorder: MediaRecorder? = null
private var recording: File? = null
private val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS)
.callTimeout(32, TimeUnit.SECONDS)
.build()
fun transcribe() {
stopRecording()
fun run(recording: File): String {
val recording = this.recording ?: return
val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = run(recording)
val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
mainScope.launch {
wkt9.onCommit(transcription)
wkt9.defaultView()
}
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
}
}
@Suppress("DEPRECATION")
fun record() {
if (recorder != null) stopRecording()
recording = File.createTempFile("recording.3gp", null, wkt9.cacheDir)
recorder = MediaRecorder().also {
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
it.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
it.setOutputFile(recording)
try {
it.prepare()
it.start()
} catch (e: Exception) {
Log.d(tag, "Failed to start recording", e)
}
}
}
private fun stopRecording() {
recorder?.run {
stop()
reset()
release()
}
recorder = null
}
private fun run(recording: File): String {
val mediaType = "audio/3gpp".toMediaType()
val requestBody = MultipartBody.Builder()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.MessageLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom"
android:background="@color/black"
android:orientation="horizontal">
<ImageView
android:layout_height="40dp"
android:layout_width="wrap_content"
android:src="@drawable/mic"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Recording..." />
</net.mezimmah.wkt9.layout.MessageLayout>

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400" />

View File

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

View File

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

View File

@@ -3,17 +3,32 @@
<string name="app_preferences_name">WKT9 Preferences</string>
<string name="preference_category_speech_to_text_name">Speech to Text</string>
<string name="preference_setting_speech_to_text_key">speech_to_text</string>
<string name="preference_setting_speech_to_text_title">Enable Speech to Text</string>
<string name="preference_setting_speech_to_text_summary">For this feature to work net.mezimmah.wkt9.WKT9 needs permission to show notifications and record audio. You will be asked to grant these permissions if you haven\'t already permitted it.</string>
<string name="preference_setting_whisper_url_key">whisper_url</string>
<string name="preference_setting_whisper_url_title">Whisper Server URL</string>
<string name="preference_setting_whisper_url_summary">Provide an URL to the Whisper server.</string>
<string name="speech_to_text">speech_to_text</string>
<string name="whisper_url">whisper_url</string>
<string name="overlay">overlay</string>
<string name="compose_timeout">compose_timeout</string>
<string-array name="input_mode_numeric">
<item>org.linphone</item>
</string-array>
<string-array name="camera_apps">
<item>com.android.camera2</item>
</string-array>
<string-array name="timeout_keys">
<item>Very short</item>
<item>Short</item>
<item>Medium</item>
<item>Long</item>
<item>Very long</item>
</string-array>
<string-array name="timeout_values">
<item>300</item>
<item>400</item>
<item>600</item>
<item>800</item>
<item>1000</item>
</string-array>
</resources>

View File

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

View File

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