Slowly getting stable

This commit is contained in:
Nehemiah of Zebulun 2023-12-03 19:58:54 -05:00
parent 6c59ee4d82
commit 92a4ac541c
13 changed files with 278 additions and 144 deletions

View File

@ -15,7 +15,7 @@ interface IME {
current: Int? = 0
)
fun onWords(words: List<Word>)
fun onWords(words: List<Word>, capMode: Int?)
fun onNextWord()
@ -23,6 +23,8 @@ interface IME {
fun onWordSelected(word: Word)
fun onDeleteWord(word: Word)
fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0)
fun onCompose(text: CharSequence)

View File

@ -2,8 +2,10 @@ 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
@ -12,14 +14,15 @@ 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.InputModeManager
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
@ -45,6 +48,8 @@ class WKT9IME: IME, InputMethodService() {
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)
}
@ -103,6 +108,10 @@ class WKT9IME: IME, InputMethodService() {
deleteText(beforeCursor, afterCursor)
}
override fun onDeleteWord(word: Word) {
inputHandler?.onDeleteWord(word)
}
override fun onGetTextBeforeCursor(n: Int): CharSequence? {
return this.currentInputConnection?.getTextBeforeCursor(n, 0)
}
@ -264,12 +273,21 @@ class WKT9IME: IME, InputMethodService() {
else showStatusIcon(icon)
}
override fun onWords(words: List<Word>) {
override fun onWords(words: List<Word>, capMode: Int?) {
this.capMode = capMode
wordsView?.words = words
}
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)
}
@ -301,7 +319,15 @@ class WKT9IME: IME, InputMethodService() {
private fun switchInputMode(mode: InputMode) {
inputHandler = when(mode) {
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)
else -> IdleInputHandler(this, this)
}

View File

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

@ -3,79 +3,73 @@ package net.mezimmah.wkt9.inputhandler
import android.text.InputType
import android.view.KeyEvent
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.KeyEventStat
import net.mezimmah.wkt9.keypad.KeyLayout
import net.mezimmah.wkt9.keypad.Keypad
import java.util.Locale
open class DefaultInputHandler(
private val wkt9: IME,
private val context: WKT9IME
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 tag = "WKT9"
protected val keypad: Keypad = Keypad()
protected var keypad: Keypad = Keypad()
protected var capMode: Int = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
protected var wordStart: Boolean = true
protected var sentenceStart: Boolean = true
protected open fun capMode(key: Key): Int? {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
null
)
var index = modes.indexOf(capMode)
init {
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 = modes.count()
if (index == 0) index = capModes.count()
index--
}
else -> index++
}
return modes[index % modes.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
currentCapMode = capModes[index % capModes.count()]
}
protected fun triggerKeyEvent(keyCode: Int) {

View File

@ -6,6 +6,7 @@ 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
@ -15,12 +16,14 @@ import java.util.Locale
class IdleInputHandler(
val ime: IME,
val wkt9: WKT9IME,
) : DefaultInputHandler(ime, wkt9), InputHandler {
) : DefaultInputHandler(wkt9), InputHandler {
init {
wkt9.showStatusIcon(R.drawable.idle_input)
}
override fun onFinishComposing() {}
override fun onLongClickCandidate(text: String) {
TODO("Not yet implemented")
}
override fun onDeleteWord(word: Word) {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {

View File

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

View File

@ -30,23 +30,22 @@ class LetterInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
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 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 sentenceStart: Boolean = false
private var wordStart: Boolean = false
private val timeoutScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var timeoutJob: Job? = null
private val content = StringBuilder()
init {
val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
@ -56,44 +55,67 @@ class LetterInputHandler(
this,
false
)
updateIcon()
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
timeoutJob?.cancel()
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
val index = stats.repeats % ends.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null
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 += ends[lastIndex].length
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
wkt9.onCommit(ends[index], beforeCursor)
wordStart = true
sentenceStart = index in 1..3
wkt9.onCommit(punctuationMarks[index], beforeCursor)
updateIcon()
}
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)
}
}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onDeleteWord(word: Word) {}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onFinishComposing() {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
override fun onFinishComposing() {
}
override fun onLongClickCandidate(text: String) {
TODO("Not yet implemented")
updateIcon()
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> capMode(key)
Command.CHARACTER -> composeCharacter(key, stats)
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
@ -106,35 +128,75 @@ class LetterInputHandler(
override fun onWordSelected(word: Word) {}
private fun composeCharacter(key: Key, stats: KeyEventStat) {
if (lastKey == key) repeats++
else {
wkt9.onCommit()
private fun composeCharacter(key: Key) {
val composing = timeoutJob?.isActive ?: false
lastKey = key
repeats = 0
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()
}
wkt9.onCompose(layout[index].toString())
lastComposeChar = layout[index]
word.append(char)
wkt9.onCompose(char)
setComposeTimeout()
}
private fun resetKey(key: Key? = null) {
lastKey = key
repeats = 0
}
private fun finishComposingChar() {
val wordDelimiters = listOf(' ', ',', ':', ';')
val sentenceDelimiters = listOf('.', '?', '!')
wkt9.onCommit()
if (sentenceDelimiters.contains(lastComposeChar)) {
sentenceStart = true
wordStart = true
} else if (wordDelimiters.contains(lastComposeChar)) {
sentenceStart = false
wordStart = true
} else {
wordStart = false
sentenceStart = false
}
updateIcon()
}
private fun setComposeTimeout() {
timeoutJob?.cancel()
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
timeoutJob = timeoutScope.launch {
delay(400L)
wkt9.onCommit("")
lastKey = null
repeats = 0
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("")
@ -149,14 +211,18 @@ class LetterInputHandler(
}
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_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
}
// 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 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
@ -17,9 +18,13 @@ class NumberInputHandler(
) : 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 onLongClickCandidate(text: String) {}
override fun onDeleteWord(word: Word) {}
override fun onSwitchLocale(locale: Locale) {}
override fun onWordSelected(word: Word) {}

View File

@ -1,6 +1,7 @@
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
@ -23,7 +24,7 @@ class WordInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
) : DefaultInputHandler(ime, wkt9), InputHandler {
) : DefaultInputHandler(wkt9), InputHandler {
private val codeword = StringBuilder()
private var staleCodeword = false
private var lastSelectedWord: Word? = null
@ -35,11 +36,17 @@ class WordInputHandler(
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var queryJob: Job? = null
override fun capMode(key: Key): Int? {
return super.capMode(key)
init {
updateIcon()
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
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
@ -48,10 +55,24 @@ class WordInputHandler(
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() {
@ -63,17 +84,9 @@ class WordInputHandler(
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) {
when (command) {
// Command.CAP_MODE -> capMode(key)
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete()
Command.ENTER -> enter(key)
@ -109,7 +122,8 @@ class WordInputHandler(
handleCodewordChange(codeword)
} else if (codeword.isNotEmpty()) {
codeword.clear()
wkt9.onCompose("")
wkt9.onCommit()
wkt9.onDeleteText(1)
} else {
wkt9.onDeleteText(1)
}
@ -129,7 +143,7 @@ class WordInputHandler(
// The codeword is stale when it does not yield any candidates in the DB
staleCodeword = words.isEmpty()
if (words.isNotEmpty()) wkt9.onWords(words)
if (words.isNotEmpty()) wkt9.onWords(words, effectiveCapMode())
}
}
@ -154,15 +168,16 @@ class WordInputHandler(
@SuppressLint("DiscouragedApi")
private fun updateIcon() {
// val mode = when (capMode) {
// InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper"
// InputType.TYPE_TEXT_FLAG_CAP_WORDS,
// InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) "cap" else "lower"
// else -> "lower"
// }
// val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
// val icon = context.resources.getIdentifier(name, "drawable", context.packageName)
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"
}
// wkt9.onUpdateStatusIcon(icon)
val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
val icon = wkt9.resources.getIdentifier(name, "drawable", wkt9.packageName)
wkt9.onUpdateStatusIcon(icon)
}
}

View File

@ -133,8 +133,7 @@ enum class Key(
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
inputModes = listOf(InputMode.Number, InputMode.Idle),
overrideConsume = true,
consume = null
)

View File

@ -1,10 +1,8 @@
package net.mezimmah.wkt9.layout
import android.annotation.SuppressLint
import android.app.PendingIntent.getActivity
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.HorizontalScrollView
@ -16,7 +14,6 @@ import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.entity.Word
class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private val tag = "WKT9"
private var wkt9: WKT9IME
private var wordCount: Int = 0
private var current: Int = 0
@ -94,7 +91,19 @@ class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(
}
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
}

View File

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

View File

@ -20,6 +20,12 @@
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"
@ -32,4 +38,10 @@
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>