Compare commits

...

10 Commits

22 changed files with 457 additions and 250 deletions

Binary file not shown.

View File

@@ -15,7 +15,7 @@ interface IME {
current: Int? = 0 current: Int? = 0
) )
fun onWords(words: List<Word>) fun onWords(words: List<Word>, capMode: Int?)
fun onNextWord() fun onNextWord()
@@ -23,15 +23,21 @@ interface IME {
fun onWordSelected(word: Word) fun onWordSelected(word: Word)
fun onDeleteWord(word: Word)
fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0) fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0)
fun onCompose(text: CharSequence) fun onCompose(text: CharSequence)
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false) fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false)
fun onGetTextBeforeCursor(n: Int): CharSequence? fun defaultView()
fun onSwitchInputHandler(inputMode: InputMode) fun onSwitchInputHandler(inputMode: InputMode)
fun onUpdateStatusIcon(icon: Int?) fun onUpdateStatusIcon(icon: Int?)
fun record()
fun transcribe()
} }

View File

@@ -2,8 +2,10 @@ package net.mezimmah.wkt9
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.provider.Settings import android.provider.Settings
import android.text.InputType
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
@@ -12,19 +14,23 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype
import androidx.preference.PreferenceManager
import net.mezimmah.wkt9.entity.Word import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputhandler.IdleInputHandler import net.mezimmah.wkt9.inputhandler.IdleInputHandler
import net.mezimmah.wkt9.inputhandler.InputHandler import net.mezimmah.wkt9.inputhandler.InputHandler
import net.mezimmah.wkt9.inputhandler.LetterInputHandler import net.mezimmah.wkt9.inputhandler.LetterInputHandler
import net.mezimmah.wkt9.inputhandler.NumberInputHandler import net.mezimmah.wkt9.inputhandler.NumberInputHandler
import net.mezimmah.wkt9.inputhandler.WordInputHandler import net.mezimmah.wkt9.inputhandler.WordInputHandler
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.inputmode.InputMode import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.keypad.Event import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.layout.Words 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.t9.T9
import net.mezimmah.wkt9.voice.Whisper
import java.util.Locale import java.util.Locale
@@ -32,11 +38,14 @@ class WKT9IME: IME, InputMethodService() {
private val tag = "WKT9" private val tag = "WKT9"
private val inputModeManager = InputModeManager(this) private val inputModeManager = InputModeManager(this)
private val whisper: Whisper = Whisper(this)
private lateinit var locale: Locale private lateinit var locale: Locale
private var inputHandler: InputHandler? = null private var inputHandler: InputHandler? = null
private var wordsView: Words? = null private var wordsLayoutView: WordsLayout? = null
private var loadingLayoutView: LoadingLayout? = null
private var messageLayoutView: MessageLayout? = null
private val keyDownStats = KeyEventStat(0, 0) private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0) private val keyUpStats = KeyEventStat(0, 0)
@@ -45,6 +54,8 @@ class WKT9IME: IME, InputMethodService() {
private var selectionStart: Int = 0 private var selectionStart: Int = 0
private var selectionEnd: Int = 0 private var selectionEnd: Int = 0
private var capMode: Int? = null
override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) { override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) {
// this.candidates?.load(candidates, current) // this.candidates?.load(candidates, current)
} }
@@ -83,9 +94,11 @@ class WKT9IME: IME, InputMethodService() {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onCreateInputView(): View? { override fun onCreateInputView(): View? {
wordsView = layoutInflater.inflate(R.layout.words, null) as Words 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 wordsView return wordsLayoutView
} }
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) { override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
@@ -103,8 +116,8 @@ class WKT9IME: IME, InputMethodService() {
deleteText(beforeCursor, afterCursor) deleteText(beforeCursor, afterCursor)
} }
override fun onGetTextBeforeCursor(n: Int): CharSequence? { override fun onDeleteWord(word: Word) {
return this.currentInputConnection?.getTextBeforeCursor(n, 0) inputHandler?.onDeleteWord(word)
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -264,21 +277,30 @@ class WKT9IME: IME, InputMethodService() {
else showStatusIcon(icon) else showStatusIcon(icon)
} }
override fun onWords(words: List<Word>) { override fun onWords(words: List<Word>, capMode: Int?) {
wordsView?.words = words this.capMode = capMode
wordsLayoutView?.words = words
} }
override fun onWordSelected(word: Word) { override fun onWordSelected(word: Word) {
this.onCompose(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) this.inputHandler?.onWordSelected(word)
} }
override fun onNextWord() { override fun onNextWord() {
wordsView?.next() wordsLayoutView?.next()
} }
override fun onPreviousWord() { override fun onPreviousWord() {
wordsView?.previous() wordsLayoutView?.previous()
} }
private fun deleteText(beforeCursor: Int, afterCursor: Int) { private fun deleteText(beforeCursor: Int, afterCursor: Int) {
@@ -287,8 +309,22 @@ class WKT9IME: IME, InputMethodService() {
} }
} }
override fun record() {
setInputView(messageLayoutView)
whisper.record()
}
override fun transcribe() {
setInputView(loadingLayoutView)
whisper.transcribe()
}
override fun defaultView() {
setInputView(wordsLayoutView)
}
private fun finishComposing() { private fun finishComposing() {
wordsView?.clear() wordsLayoutView?.clear()
inputHandler?.onFinishComposing() inputHandler?.onFinishComposing()
} }
@@ -301,7 +337,15 @@ class WKT9IME: IME, InputMethodService() {
private fun switchInputMode(mode: InputMode) { private fun switchInputMode(mode: InputMode) {
inputHandler = when(mode) { inputHandler = when(mode) {
InputMode.Word -> WordInputHandler(this, this, locale) InputMode.Word -> WordInputHandler(this, this, locale)
InputMode.Letter -> LetterInputHandler(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) InputMode.Number -> NumberInputHandler(this, this)
else -> IdleInputHandler(this, this) else -> IdleInputHandler(this, this)
} }

View File

@@ -11,8 +11,8 @@ interface WordDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(vararg words: Word) suspend fun insert(vararg words: Word)
@Query("DELETE FROM word WHERE word = :word AND locale = :locale") @Query("DELETE FROM word WHERE id = :id")
fun delete(word: String, locale: String) fun delete(id: Int)
@Query("SELECT * FROM word WHERE code LIKE :code || '%' AND locale = :locale " + @Query("SELECT * FROM word WHERE code LIKE :code || '%' AND locale = :locale " +
"ORDER BY length, weight DESC LIMIT :limit") "ORDER BY length, weight DESC LIMIT :limit")
@@ -20,4 +20,7 @@ interface WordDao {
@Query("UPDATE word SET weight = weight + 1 WHERE id=:id") @Query("UPDATE word SET weight = weight + 1 WHERE id=:id")
suspend fun increaseWeight(id: Int) 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

@@ -3,79 +3,77 @@ package net.mezimmah.wkt9.inputhandler
import android.text.InputType import android.text.InputType
import android.view.KeyEvent import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.inputmode.TextPositionInfo
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import net.mezimmah.wkt9.keypad.Keypad import net.mezimmah.wkt9.keypad.Keypad
import java.util.Locale
open class DefaultInputHandler( open class DefaultInputHandler(
private val wkt9: IME, private val wkt9: WKT9IME
private val context: WKT9IME
) { ) {
protected val tag = "WKT9" private val capModes = listOf(
protected var keypad: Keypad = Keypad()
protected var capMode: Int = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
protected open fun capMode(key: Key): Int? {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES, InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS, InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
null null
) )
var index = modes.indexOf(capMode)
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) { when (key) {
Key.B2 -> { Key.B2 -> {
if (index == 0) index = modes.count() if (index == 0) index = capModes.count()
index-- index--
} }
else -> index++ else -> index++
} }
return modes[index % modes.count()] currentCapMode = capModes[index % capModes.count()]
}
protected open fun finalizeWordOrSentence(stats: KeyEventStat) {
val candidates = ArrayList<CharSequence>()
wkt9.onCandidates(
candidates = candidates,
current = stats.repeats % candidates.count()
)
}
protected open fun getTextPositionInfo(text: CharSequence): TextPositionInfo {
val trimmed = text.trimEnd()
val regex = "[.!?]$".toRegex()
val startSentence = text.isEmpty() || regex.containsMatchIn(trimmed)
val startWord = text.isEmpty() || (startSentence || trimmed.length < text.length)
return TextPositionInfo(
startSentence = startSentence,
startWord = startWord
)
}
protected fun getDefaultCapMode(typeFlags: Int): Int? {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
)
modes.forEach {
if (typeFlags.and(it) == it) return it
}
return null
} }
protected fun triggerKeyEvent(keyCode: Int) { protected fun triggerKeyEvent(keyCode: Int) {
@@ -89,4 +87,27 @@ open class DefaultInputHandler(
protected fun triggerOriginalKeyEvent(key: Key) { protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode) 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

@@ -6,8 +6,8 @@ import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat import net.mezimmah.wkt9.keypad.KeyEventStat
@@ -15,13 +15,15 @@ import java.util.Locale
class IdleInputHandler( class IdleInputHandler(
val ime: IME, val ime: IME,
private var wkt9: WKT9IME, val wkt9: WKT9IME,
) : DefaultInputHandler(ime, wkt9), InputHandler { ) : DefaultInputHandler(wkt9), InputHandler {
init {
wkt9.showStatusIcon(R.drawable.idle_input)
}
override fun onFinishComposing() {} override fun onFinishComposing() {}
override fun onLongClickCandidate(text: String) { override fun onDeleteWord(word: Word) {}
TODO("Not yet implemented")
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) { override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) { when (command) {

View File

@@ -8,9 +8,9 @@ import net.mezimmah.wkt9.keypad.KeyEventStat
import java.util.Locale import java.util.Locale
interface InputHandler { interface InputHandler {
fun onFinishComposing() fun onDeleteWord(word: Word)
fun onLongClickCandidate(text: String) fun onFinishComposing()
fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat)

View File

@@ -30,23 +30,22 @@ class LetterInputHandler(
val ime: IME, val ime: IME,
private var wkt9: WKT9IME, private var wkt9: WKT9IME,
private var locale: Locale, private var locale: Locale,
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(ime, wkt9), InputHandler { private var composeTimeout: Long,
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(wkt9), InputHandler {
private val db = AppDatabase.getInstance(wkt9) private val db = AppDatabase.getInstance(wkt9)
private val wordDao = db.getWordDao() private val wordDao = db.getWordDao()
private val word = StringBuilder()
private var lastKey: Key? = null private var lastKey: Key? = null
private var repeats: Int = 0 private var repeats: Int = 0
private var lastComposeChar: Char? = null
private var lastIcon: Int? = null
private var spellCheckerSession: SpellCheckerSession? = null private var spellCheckerSession: SpellCheckerSession? = null
private var sentenceStart: Boolean = false
private var wordStart: Boolean = false
private val timeoutScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var timeoutJob: Job? = null private var timeoutJob: Job? = null
private val content = StringBuilder()
init { init {
val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
@@ -56,85 +55,157 @@ class LetterInputHandler(
this, this,
false false
) )
updateIcon()
} }
override fun finalizeWordOrSentence(stats: KeyEventStat) { override fun onDeleteWord(word: Word) {}
timeoutJob?.cancel()
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ") override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
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 override fun onFinishComposing() {}
wkt9.onCommit(ends[index], beforeCursor)
}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onSwitchLocale(locale: Locale) { override fun onSwitchLocale(locale: Locale) {
this.locale = locale this.locale = locale
} }
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onFinishComposing() {
}
override fun onLongClickCandidate(text: String) {
TODO("Not yet implemented")
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) { override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) { when (command) {
Command.CAP_MODE -> capMode(key) Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key, stats) Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete() Command.DELETE -> delete()
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key) Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key) Command.NUMBER -> triggerOriginalKeyEvent(key)
// Command.RECORD -> wkt9.onRecord(true) Command.RECORD -> wkt9.record()
Command.SPACE -> finalizeWordOrSentence(stats) Command.SPACE -> finalizeWordOrSentence(stats)
// Command.TRANSCRIBE -> wkt9.onTranscribe() Command.TRANSCRIBE -> wkt9.transcribe()
else -> Log.d(tag, "Command not implemented: $command") else -> Log.d(tag, "Command not implemented: $command")
} }
} }
override fun onWordSelected(word: Word) {} override fun onWordSelected(word: Word) {}
private fun composeCharacter(key: Key, stats: KeyEventStat) { override fun toggleCapMode(key: Key) {
if (lastKey == key) repeats++ super.toggleCapMode(key)
else {
wkt9.onCommit()
lastKey = key updateIcon()
repeats = 0 }
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 layout = KeyLayout.chars[key] ?: return
val index = repeats % layout.count() val index = repeats % layout.count()
val char = when(effectiveCapMode()) {
null -> layout[index].toString()
else -> layout[index].uppercase()
}
wkt9.onCompose(layout[index].toString()) lastComposeChar = layout[index]
word.append(char)
wkt9.onCompose(char)
setComposeTimeout() setComposeTimeout()
} }
private fun setComposeTimeout() { private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
timeoutJob?.cancel() timeoutJob?.cancel()
timeoutJob = timeoutScope.launch { val index = stats.repeats % punctuationMarks.count()
delay(400L) val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
wkt9.onCommit("") var beforeCursor = 0
lastKey = null 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 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() { private fun delete() {
if (word.isNotEmpty()) word.deleteAt(word.length - 1)
resetKey()
if (timeoutJob?.isActive == true) { if (timeoutJob?.isActive == true) {
timeoutJob?.cancel() timeoutJob?.cancel()
wkt9.onCompose("") wkt9.onCompose("")
@@ -149,14 +220,18 @@ class LetterInputHandler(
} }
private fun updateIcon() { private fun updateIcon() {
val icon = when (capMode) { val icon = when (effectiveCapMode()) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.letter_upper InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.letter_upper
InputType.TYPE_TEXT_FLAG_CAP_WORDS, InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) R.drawable.letter_cap else R.drawable.letter_lower InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> R.drawable.letter_cap
else -> R.drawable.letter_lower else -> R.drawable.letter_lower
} }
// wkt9.onUpdateStatusIcon(icon) if (icon == lastIcon) return
wkt9.onUpdateStatusIcon(icon)
lastIcon = icon
} }
} }
// //

View File

@@ -4,6 +4,7 @@ import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command import net.mezimmah.wkt9.keypad.Command
@@ -17,9 +18,13 @@ class NumberInputHandler(
) : InputHandler { ) : InputHandler {
private val tag = "WKT9" private val tag = "WKT9"
init {
wkt9.showStatusIcon(R.drawable.number_input)
}
// We don't need to implement methods below // We don't need to implement methods below
override fun onFinishComposing() {} override fun onFinishComposing() {}
override fun onLongClickCandidate(text: String) {} override fun onDeleteWord(word: Word) {}
override fun onSwitchLocale(locale: Locale) {} override fun onSwitchLocale(locale: Locale) {}
override fun onWordSelected(word: Word) {} override fun onWordSelected(word: Word) {}

View File

@@ -1,6 +1,7 @@
package net.mezimmah.wkt9.inputhandler package net.mezimmah.wkt9.inputhandler
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.text.InputType
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -22,8 +23,8 @@ import java.util.Locale
class WordInputHandler( class WordInputHandler(
val ime: IME, val ime: IME,
private var wkt9: WKT9IME, private var wkt9: WKT9IME,
private var locale: Locale, private var locale: Locale
) : DefaultInputHandler(ime, wkt9), InputHandler { ) : DefaultInputHandler(wkt9), InputHandler {
private val codeword = StringBuilder() private val codeword = StringBuilder()
private var staleCodeword = false private var staleCodeword = false
private var lastSelectedWord: Word? = null private var lastSelectedWord: Word? = null
@@ -35,11 +36,17 @@ class WordInputHandler(
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var queryJob: Job? = null private var queryJob: Job? = null
override fun capMode(key: Key): Int? { init {
return super.capMode(key) updateIcon()
} }
override fun finalizeWordOrSentence(stats: KeyEventStat) { override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
updateIcon()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ") val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
val index = stats.repeats % ends.count() val index = stats.repeats % ends.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null
@@ -48,10 +55,24 @@ class WordInputHandler(
if (lastIndex != null) beforeCursor += ends[lastIndex].length if (lastIndex != null) beforeCursor += ends[lastIndex].length
wkt9.onCommit(ends[index], beforeCursor) 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) { override fun onSwitchLocale(locale: Locale) {
this.locale = locale this.locale = locale
updateIcon()
} }
override fun onFinishComposing() { override fun onFinishComposing() {
@@ -63,28 +84,36 @@ class WordInputHandler(
lastSelectedWord = null lastSelectedWord = null
} }
override fun onLongClickCandidate(text: String) {
ioScope.launch {
wordDao.delete(text, locale.language)
handleCodewordChange(codeword)
}
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) { override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) { when (command) {
// Command.CAP_MODE -> capMode(key) Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> buildCodeword(key) Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete() Command.DELETE -> delete(event.repeatCount)
Command.ENTER -> enter(key) Command.ENTER -> enter(key)
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key) Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor(key) Command.MOVE_CURSOR -> moveCursor(key)
Command.NUMBER -> triggerOriginalKeyEvent(key) Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats) Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> transcribe()
else -> Log.d(tag, "Command not implemented: $command") 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) { override fun onWordSelected(word: Word) {
lastSelectedWord = word lastSelectedWord = word
} }
@@ -101,12 +130,15 @@ class WordInputHandler(
handleCodewordChange(codeword) handleCodewordChange(codeword)
} }
private fun delete() { private fun delete(repeatCount: Int) {
lastSelectedWord = null lastSelectedWord = null
if (codeword.length > 1) { if (codeword.length > 1) {
codeword.deleteAt(codeword.length - 1) codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword) handleCodewordChange(codeword)
} else if (codeword.isNotEmpty() && repeatCount > 1) {
codeword.clear()
wkt9.onCompose("")
} else if (codeword.isNotEmpty()) { } else if (codeword.isNotEmpty()) {
codeword.clear() codeword.clear()
wkt9.onCompose("") wkt9.onCompose("")
@@ -115,6 +147,11 @@ class WordInputHandler(
} }
} }
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun enter(key: Key) { private fun enter(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit("") if (codeword.isNotEmpty()) wkt9.onCommit("")
else triggerOriginalKeyEvent(key) else triggerOriginalKeyEvent(key)
@@ -129,7 +166,7 @@ class WordInputHandler(
// The codeword is stale when it does not yield any candidates in the DB // The codeword is stale when it does not yield any candidates in the DB
staleCodeword = words.isEmpty() staleCodeword = words.isEmpty()
if (words.isNotEmpty()) wkt9.onWords(words) if (words.isNotEmpty()) wkt9.onWords(words, effectiveCapMode())
} }
} }
@@ -142,6 +179,8 @@ class WordInputHandler(
} }
private fun inputMode(key: Key) { private fun inputMode(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit()
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number) if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number)
else wkt9.onSwitchInputHandler(InputMode.Letter) else wkt9.onSwitchInputHandler(InputMode.Letter)
} }
@@ -154,15 +193,16 @@ class WordInputHandler(
@SuppressLint("DiscouragedApi") @SuppressLint("DiscouragedApi")
private fun updateIcon() { private fun updateIcon() {
// val mode = when (capMode) { val mode = when (effectiveCapMode()) {
// InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper" InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper"
// InputType.TYPE_TEXT_FLAG_CAP_WORDS, InputType.TYPE_TEXT_FLAG_CAP_WORDS,
// InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) "cap" else "lower" InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> "cap"
// else -> "lower" else -> "lower"
// } }
// val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
// val icon = context.resources.getIdentifier(name, "drawable", context.packageName)
// wkt9.onUpdateStatusIcon(icon) val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
val icon = wkt9.resources.getIdentifier(name, "drawable", wkt9.packageName)
wkt9.onUpdateStatusIcon(icon)
} }
} }

View File

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

View File

@@ -133,8 +133,7 @@ enum class Key(
), ),
CommandMapping( CommandMapping(
events = listOf(Event.keyDown), inputModes = listOf(InputMode.Number, InputMode.Idle),
inputModes = listOf(InputMode.Number),
overrideConsume = true, overrideConsume = true,
consume = null consume = null
) )
@@ -389,6 +388,12 @@ enum class Key(
command = Command.DELETE command = Command.DELETE
), ),
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Word, InputMode.Letter),
command = Command.FINISH_DELETE
),
CommandMapping( CommandMapping(
inputModes = listOf(InputMode.Number), inputModes = listOf(InputMode.Number),
overrideConsume = true, overrideConsume = true,

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

@@ -1,10 +1,8 @@
package net.mezimmah.wkt9.layout package net.mezimmah.wkt9.layout
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent.getActivity
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.View import android.view.View
import android.widget.HorizontalScrollView import android.widget.HorizontalScrollView
@@ -15,8 +13,7 @@ import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.entity.Word import net.mezimmah.wkt9.entity.Word
class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener { class WordsLayout(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private val tag = "WKT9"
private var wkt9: WKT9IME private var wkt9: WKT9IME
private var wordCount: Int = 0 private var wordCount: Int = 0
private var current: Int = 0 private var current: Int = 0
@@ -94,7 +91,19 @@ class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(
} }
override fun onLongClick(v: View?): Boolean { override fun onLongClick(v: View?): Boolean {
Log.d(tag, "We need to delete this word from the db") 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 return true
} }

View File

@@ -2,17 +2,12 @@ package net.mezimmah.wkt9.voice
import android.media.MediaRecorder import android.media.MediaRecorder
import android.util.Log import android.util.Log
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.inputhandler.InputHandler
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -23,17 +18,11 @@ import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class Whisper( class Whisper(
private val context: WKT9IME, private val wkt9: WKT9IME,
private val inputHandler: InputHandler?,
private val ui: View
) { ) {
private val tag = "WKT9" private val tag = "WKT9"
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var ioJob: Job? = null private var ioJob: Job? = null
private var recorder: MediaRecorder? = null private var recorder: MediaRecorder? = null
private var recording: File? = null private var recording: File? = null
@@ -47,19 +36,18 @@ class Whisper(
stopRecording() stopRecording()
val recording = this.recording ?: return val recording = this.recording ?: return
val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
showTranscribing()
ioJob?.cancel() ioJob?.cancel()
ioJob = ioScope.launch { ioJob = ioScope.launch {
try { try {
val transcription = run(recording) val transcription = run(recording)
val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
mainScope.launch { mainScope.launch {
showCandidates() wkt9.onCommit(transcription)
wkt9.defaultView()
} }
// inputHandler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) { } catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e) Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
} }
@@ -70,9 +58,7 @@ class Whisper(
fun record() { fun record() {
if (recorder != null) stopRecording() if (recorder != null) stopRecording()
showMessage() recording = File.createTempFile("recording.3gp", null, wkt9.cacheDir)
recording = File.createTempFile("recording.3gp", null, context.cacheDir)
recorder = MediaRecorder().also { recorder = MediaRecorder().also {
it.setAudioSource(MediaRecorder.AudioSource.MIC) it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
@@ -88,36 +74,6 @@ class Whisper(
} }
} }
private fun showCandidates() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.VISIBLE
// loadingView.visibility = View.GONE
// messageView.visibility = View.GONE
}
private fun showMessage() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.GONE
// messageView.visibility = View.VISIBLE
}
private fun showTranscribing() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.VISIBLE
// messageView.visibility = View.GONE
}
private fun stopRecording() { private fun stopRecording() {
recorder?.run { recorder?.run {
stop() stop()

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

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <net.mezimmah.wkt9.layout.MessageLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="44dp" android:layout_height="44dp"
@@ -10,11 +10,11 @@
android:background="@color/black" android:background="@color/black"
android:orientation="horizontal"> android:orientation="horizontal">
<ProgressBar <ImageView
android:id="@+id/suggestions" android:layout_height="40dp"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="40dp" /> android:src="@drawable/mic"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -24,6 +24,6 @@
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:textSize="20sp" android:textSize="20sp"
android:textFontWeight="400" android:textFontWeight="400"
android:text="Transcribing, please wait..." /> android:text="Recording..." />
</LinearLayout> </net.mezimmah.wkt9.layout.MessageLayout>

View File

@@ -1,19 +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="match_parent"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400" />
</LinearLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.Words xmlns:android="http://schemas.android.com/apk/res/android" <net.mezimmah.wkt9.layout.WordsLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
@@ -14,4 +14,4 @@
android:layout_height="44dp" android:layout_height="44dp"
android:orientation="horizontal" /> android:orientation="horizontal" />
</net.mezimmah.wkt9.layout.Words> </net.mezimmah.wkt9.layout.WordsLayout>

View File

@@ -29,6 +29,6 @@
<item>400</item> <item>400</item>
<item>600</item> <item>600</item>
<item>800</item> <item>800</item>
<item>900</item> <item>1000</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -20,6 +20,12 @@
android:languageTag="es-ES" android:languageTag="es-ES"
android:imeSubtypeMode="keyboard" /> android:imeSubtypeMode="keyboard" />
<subtype
android:label="Spanish AR"
android:imeSubtypeLocale="es_AR"
android:languageTag="es-AR"
android:imeSubtypeMode="keyboard" />
<subtype <subtype
android:label="German DE" android:label="German DE"
android:imeSubtypeLocale="de_DE" android:imeSubtypeLocale="de_DE"
@@ -32,4 +38,10 @@
android:languageTag="pt-PT" android:languageTag="pt-PT"
android:imeSubtypeMode="keyboard" /> android:imeSubtypeMode="keyboard" />
<subtype
android:label="Portuguese BR"
android:imeSubtypeLocale="pt_BR"
android:languageTag="pt-BR"
android:imeSubtypeMode="keyboard" />
</input-method> </input-method>