474 lines
20 KiB
Kotlin
474 lines
20 KiB
Kotlin
package com.simplemobiletools.keyboard.services
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.SharedPreferences
|
|
import android.graphics.drawable.Icon
|
|
import android.graphics.drawable.LayerDrawable
|
|
import android.graphics.drawable.RippleDrawable
|
|
import android.icu.text.BreakIterator
|
|
import android.icu.util.ULocale
|
|
import android.inputmethodservice.InputMethodService
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.text.InputType.*
|
|
import android.text.TextUtils
|
|
import android.util.Size
|
|
import android.view.KeyEvent
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.inputmethod.*
|
|
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
|
|
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
|
|
import android.view.inputmethod.EditorInfo.IME_MASK_ACTION
|
|
import android.widget.inline.InlinePresentationSpec
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.autofill.inline.UiVersions
|
|
import androidx.autofill.inline.common.ImageViewStyle
|
|
import androidx.autofill.inline.common.TextViewStyle
|
|
import androidx.autofill.inline.common.ViewStyle
|
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
|
import androidx.core.graphics.drawable.toBitmap
|
|
import com.simplemobiletools.commons.extensions.*
|
|
import com.simplemobiletools.commons.helpers.isNougatPlus
|
|
import com.simplemobiletools.keyboard.R
|
|
import com.simplemobiletools.keyboard.databinding.KeyboardViewKeyboardBinding
|
|
import com.simplemobiletools.keyboard.extensions.config
|
|
import com.simplemobiletools.keyboard.extensions.getStrokeColor
|
|
import com.simplemobiletools.keyboard.extensions.safeStorageContext
|
|
import com.simplemobiletools.keyboard.helpers.*
|
|
import com.simplemobiletools.keyboard.interfaces.OnKeyboardActionListener
|
|
import com.simplemobiletools.keyboard.views.MyKeyboardView
|
|
import java.util.Locale
|
|
|
|
// based on https://www.androidauthority.com/lets-build-custom-keyboard-android-832362/
|
|
class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
|
private var SHIFT_PERM_TOGGLE_SPEED = 500 // how quickly do we have to doubletap shift to enable permanent caps lock
|
|
private val KEYBOARD_LETTERS = 0
|
|
private val KEYBOARD_SYMBOLS = 1
|
|
private val KEYBOARD_SYMBOLS_SHIFT = 2
|
|
private val KEYBOARD_NUMBERS = 3
|
|
private val KEYBOARD_PHONE = 4
|
|
|
|
private var keyboard: MyKeyboard? = null
|
|
private var keyboardView: MyKeyboardView? = null
|
|
private var lastShiftPressTS = 0L
|
|
private var keyboardMode = KEYBOARD_LETTERS
|
|
private var inputTypeClass = TYPE_CLASS_TEXT
|
|
private var inputTypeClassVariation = TYPE_CLASS_TEXT
|
|
private var enterKeyType = IME_ACTION_NONE
|
|
private var switchToLetters = false
|
|
private var breakIterator: BreakIterator? = null
|
|
|
|
private lateinit var binding: KeyboardViewKeyboardBinding
|
|
|
|
override fun onInitializeInterface() {
|
|
super.onInitializeInterface()
|
|
safeStorageContext.getSharedPrefs().registerOnSharedPreferenceChangeListener(this)
|
|
}
|
|
|
|
override fun onCreateInputView(): View {
|
|
binding = KeyboardViewKeyboardBinding.inflate(layoutInflater)
|
|
keyboardView = binding.keyboardView.apply {
|
|
setKeyboardHolder(binding)
|
|
setKeyboard(keyboard!!)
|
|
setEditorInfo(currentInputEditorInfo)
|
|
mOnKeyboardActionListener = this@SimpleKeyboardIME
|
|
}
|
|
return binding.root
|
|
}
|
|
|
|
override fun onPress(primaryCode: Int) {
|
|
if (primaryCode != 0) {
|
|
keyboardView?.vibrateIfNeeded()
|
|
}
|
|
}
|
|
|
|
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
|
super.onStartInput(attribute, restarting)
|
|
inputTypeClass = attribute!!.inputType and TYPE_MASK_CLASS
|
|
inputTypeClassVariation = attribute.inputType and TYPE_MASK_VARIATION
|
|
enterKeyType = attribute.imeOptions and (IME_MASK_ACTION or IME_FLAG_NO_ENTER_ACTION)
|
|
keyboard = createNewKeyboard()
|
|
keyboardView?.setKeyboard(keyboard!!)
|
|
keyboardView?.setEditorInfo(attribute)
|
|
if (isNougatPlus()) {
|
|
breakIterator = BreakIterator.getCharacterInstance(ULocale.getDefault())
|
|
}
|
|
updateShiftKeyState()
|
|
}
|
|
|
|
private fun updateShiftKeyState() {
|
|
if (keyboard?.mShiftState == ShiftState.ON_PERMANENT) {
|
|
return
|
|
}
|
|
|
|
val editorInfo = currentInputEditorInfo
|
|
if (config.enableSentencesCapitalization && editorInfo != null && editorInfo.inputType != TYPE_NULL) {
|
|
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
|
|
keyboard?.setShifted(ShiftState.ON_ONE_CHAR)
|
|
keyboardView?.invalidateAllKeys()
|
|
return
|
|
}
|
|
}
|
|
|
|
keyboard?.setShifted(ShiftState.OFF)
|
|
keyboardView?.invalidateAllKeys()
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest {
|
|
val maxWidth = resources.getDimensionPixelSize(R.dimen.suggestion_max_width)
|
|
|
|
return InlineSuggestionsRequest.Builder(
|
|
listOf(
|
|
InlinePresentationSpec.Builder(
|
|
Size(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT),
|
|
Size(maxWidth, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
).setStyle(buildSuggestionTextStyle()).build()
|
|
)
|
|
).setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
|
|
.build()
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
|
|
keyboardView?.clearClipboardViews()
|
|
|
|
response.inlineSuggestions.forEach {
|
|
it.inflate(this, Size(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT), this.mainExecutor) { view ->
|
|
// If inflation fails for whatever reason, passed view will be null
|
|
if (view != null) {
|
|
keyboardView?.addToClipboardViews(view, addToFront = it.info.isPinned)
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
override fun onKey(code: Int) {
|
|
val inputConnection = currentInputConnection
|
|
if (keyboard == null || inputConnection == null) {
|
|
return
|
|
}
|
|
|
|
if (code != MyKeyboard.KEYCODE_SHIFT) {
|
|
lastShiftPressTS = 0
|
|
}
|
|
|
|
when (code) {
|
|
MyKeyboard.KEYCODE_DELETE -> {
|
|
val selectedText = inputConnection.getSelectedText(0)
|
|
if (TextUtils.isEmpty(selectedText)) {
|
|
val count = getCountToDelete(inputConnection)
|
|
inputConnection.deleteSurroundingText(count, 0)
|
|
} else {
|
|
inputConnection.commitText("", 1)
|
|
}
|
|
}
|
|
|
|
MyKeyboard.KEYCODE_SHIFT -> {
|
|
if (keyboardMode == KEYBOARD_LETTERS) {
|
|
when {
|
|
keyboard!!.mShiftState == ShiftState.ON_PERMANENT -> keyboard!!.mShiftState = ShiftState.OFF
|
|
System.currentTimeMillis() - lastShiftPressTS < SHIFT_PERM_TOGGLE_SPEED -> keyboard!!.mShiftState = ShiftState.ON_PERMANENT
|
|
keyboard!!.mShiftState == ShiftState.ON_ONE_CHAR -> keyboard!!.mShiftState = ShiftState.OFF
|
|
keyboard!!.mShiftState == ShiftState.OFF -> keyboard!!.mShiftState = ShiftState.ON_ONE_CHAR
|
|
}
|
|
|
|
lastShiftPressTS = System.currentTimeMillis()
|
|
} else {
|
|
val keyboardXml = if (keyboardMode == KEYBOARD_SYMBOLS) {
|
|
keyboardMode = KEYBOARD_SYMBOLS_SHIFT
|
|
R.xml.keys_symbols_shift
|
|
} else {
|
|
keyboardMode = KEYBOARD_SYMBOLS
|
|
R.xml.keys_symbols
|
|
}
|
|
keyboard = MyKeyboard(this, keyboardXml, enterKeyType)
|
|
keyboardView!!.setKeyboard(keyboard!!)
|
|
}
|
|
keyboardView!!.invalidateAllKeys()
|
|
}
|
|
|
|
MyKeyboard.KEYCODE_ENTER -> {
|
|
val imeOptionsActionId = getImeOptionsActionId()
|
|
if (imeOptionsActionId != IME_ACTION_NONE) {
|
|
inputConnection.performEditorAction(imeOptionsActionId)
|
|
} else {
|
|
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
|
|
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
|
|
}
|
|
}
|
|
|
|
MyKeyboard.KEYCODE_MODE_CHANGE -> {
|
|
val keyboardXml = if (keyboardMode == KEYBOARD_LETTERS) {
|
|
keyboardMode = KEYBOARD_SYMBOLS
|
|
R.xml.keys_symbols
|
|
} else {
|
|
keyboardMode = KEYBOARD_LETTERS
|
|
getKeyboardLayoutXML()
|
|
}
|
|
keyboard = MyKeyboard(this, keyboardXml, enterKeyType)
|
|
keyboardView!!.setKeyboard(keyboard!!)
|
|
}
|
|
|
|
MyKeyboard.KEYCODE_EMOJI -> {
|
|
keyboardView?.openEmojiPalette()
|
|
}
|
|
|
|
else -> {
|
|
var codeChar = code.toChar()
|
|
val originalText = inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text
|
|
|
|
if (Character.isLetter(codeChar) && keyboard!!.mShiftState > ShiftState.OFF) {
|
|
if (baseContext.config.keyboardLanguage == LANGUAGE_TURKISH_Q) {
|
|
codeChar = codeChar.toString().uppercase(Locale.forLanguageTag("tr")).single()
|
|
} else {
|
|
codeChar = Character.toUpperCase(codeChar)
|
|
}
|
|
}
|
|
|
|
// If the keyboard is set to symbols and the user presses space, we usually should switch back to the letters keyboard.
|
|
// However, avoid doing that in cases when the EditText for example requires numbers as the input.
|
|
// We can detect that by the text not changing on pressing Space.
|
|
if (keyboardMode != KEYBOARD_LETTERS && inputTypeClass == TYPE_CLASS_TEXT && code == MyKeyboard.KEYCODE_SPACE) {
|
|
inputConnection.commitText(codeChar.toString(), 1)
|
|
val newText = inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text
|
|
if (originalText != newText) {
|
|
switchToLetters = true
|
|
}
|
|
} else {
|
|
when {
|
|
!originalText.isNullOrEmpty() && cachedVNTelexData.isNotEmpty() -> {
|
|
val fullText = originalText.toString() + codeChar.toString()
|
|
val lastIndexEmpty = if (fullText.contains(" ")) {
|
|
fullText.lastIndexOf(" ")
|
|
} else 0
|
|
if (lastIndexEmpty >= 0) {
|
|
val word = fullText.subSequence(lastIndexEmpty, fullText.length).trim().toString()
|
|
val wordChars = word.toCharArray()
|
|
val predictWord = StringBuilder()
|
|
for (char in wordChars.size - 1 downTo 0) {
|
|
predictWord.append(wordChars[char])
|
|
val shouldChangeText = predictWord.reverse().toString()
|
|
if (cachedVNTelexData.containsKey(shouldChangeText)) {
|
|
inputConnection.setComposingRegion(fullText.length - shouldChangeText.length, fullText.length)
|
|
inputConnection.setComposingText(cachedVNTelexData[shouldChangeText], fullText.length)
|
|
inputConnection.setComposingRegion(fullText.length, fullText.length)
|
|
return
|
|
}
|
|
}
|
|
inputConnection.commitText(codeChar.toString(), 1)
|
|
updateShiftKeyState()
|
|
}
|
|
}
|
|
|
|
else -> {
|
|
inputConnection.commitText(codeChar.toString(), 1)
|
|
updateShiftKeyState()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getCountToDelete(inputConnection: InputConnection): Int {
|
|
if (breakIterator == null || !isNougatPlus()) {
|
|
return 1
|
|
}
|
|
|
|
val prevText = inputConnection.getTextBeforeCursor(8, 0)
|
|
|
|
|
|
if (!TextUtils.isEmpty(prevText)) {
|
|
return breakIterator?.let {
|
|
it.setText(prevText.toString())
|
|
val end = it.last()
|
|
val start = it.previous()
|
|
(end - (if (start == BreakIterator.DONE) 0 else start)).coerceIn(0, prevText?.length)
|
|
} ?: 1
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
override fun onActionUp() {
|
|
if (switchToLetters) {
|
|
// TODO: Change keyboardMode to enum class
|
|
keyboardMode = KEYBOARD_LETTERS
|
|
|
|
keyboard = MyKeyboard(this, getKeyboardLayoutXML(), enterKeyType)
|
|
|
|
val editorInfo = currentInputEditorInfo
|
|
if (editorInfo != null && editorInfo.inputType != TYPE_NULL && keyboard?.mShiftState != ShiftState.ON_PERMANENT) {
|
|
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
|
|
keyboard?.setShifted(ShiftState.ON_ONE_CHAR)
|
|
}
|
|
}
|
|
|
|
keyboardView!!.setKeyboard(keyboard!!)
|
|
switchToLetters = false
|
|
}
|
|
}
|
|
|
|
override fun moveCursorLeft() {
|
|
moveCursor(false)
|
|
}
|
|
|
|
override fun moveCursorRight() {
|
|
moveCursor(true)
|
|
}
|
|
|
|
override fun onText(text: String) {
|
|
currentInputConnection?.commitText(text, 1)
|
|
}
|
|
|
|
override fun reloadKeyboard() {
|
|
val keyboard = createNewKeyboard()
|
|
this.keyboard = keyboard
|
|
keyboardView?.setKeyboard(keyboard)
|
|
}
|
|
|
|
private fun createNewKeyboard(): MyKeyboard {
|
|
val keyboardXml = when (inputTypeClass) {
|
|
TYPE_CLASS_NUMBER -> {
|
|
keyboardMode = KEYBOARD_NUMBERS
|
|
R.xml.keys_numbers
|
|
}
|
|
|
|
TYPE_CLASS_PHONE -> {
|
|
keyboardMode = KEYBOARD_PHONE
|
|
R.xml.keys_phone
|
|
}
|
|
|
|
TYPE_CLASS_DATETIME -> {
|
|
keyboardMode = KEYBOARD_SYMBOLS
|
|
R.xml.keys_symbols
|
|
}
|
|
|
|
else -> {
|
|
keyboardMode = KEYBOARD_LETTERS
|
|
getKeyboardLayoutXML()
|
|
}
|
|
}
|
|
return MyKeyboard(
|
|
context = this,
|
|
xmlLayoutResId = keyboardXml,
|
|
enterKeyType = enterKeyType,
|
|
)
|
|
}
|
|
|
|
override fun onUpdateSelection(oldSelStart: Int, oldSelEnd: Int, newSelStart: Int, newSelEnd: Int, candidatesStart: Int, candidatesEnd: Int) {
|
|
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
|
|
if (newSelStart == newSelEnd) {
|
|
keyboardView?.closeClipboardManager()
|
|
}
|
|
updateShiftKeyState()
|
|
}
|
|
|
|
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
|
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
|
updateShiftKeyState()
|
|
}
|
|
|
|
private fun moveCursor(moveRight: Boolean) {
|
|
val extractedText = currentInputConnection?.getExtractedText(ExtractedTextRequest(), 0) ?: return
|
|
var newCursorPosition = extractedText.selectionStart
|
|
newCursorPosition = if (moveRight) {
|
|
newCursorPosition + 1
|
|
} else {
|
|
newCursorPosition - 1
|
|
}
|
|
|
|
currentInputConnection?.setSelection(newCursorPosition, newCursorPosition)
|
|
}
|
|
|
|
private fun getImeOptionsActionId(): Int {
|
|
return if (currentInputEditorInfo.imeOptions and IME_FLAG_NO_ENTER_ACTION != 0) {
|
|
IME_ACTION_NONE
|
|
} else {
|
|
currentInputEditorInfo.imeOptions and IME_MASK_ACTION
|
|
}
|
|
}
|
|
|
|
private fun getKeyboardLayoutXML(): Int {
|
|
return when (baseContext.config.keyboardLanguage) {
|
|
LANGUAGE_BENGALI -> R.xml.keys_letters_bengali
|
|
LANGUAGE_BULGARIAN -> R.xml.keys_letters_bulgarian
|
|
LANGUAGE_DANISH -> R.xml.keys_letters_danish
|
|
LANGUAGE_ENGLISH_DVORAK -> R.xml.keys_letters_english_dvorak
|
|
LANGUAGE_ENGLISH_QWERTZ -> R.xml.keys_letters_english_qwertz
|
|
LANGUAGE_FRENCH_AZERTY -> R.xml.keys_letters_french_azerty
|
|
LANGUAGE_FRENCH_BEPO -> R.xml.keys_letters_french_bepo
|
|
LANGUAGE_GERMAN -> R.xml.keys_letters_german
|
|
LANGUAGE_GREEK -> R.xml.keys_letters_greek
|
|
LANGUAGE_LITHUANIAN -> R.xml.keys_letters_lithuanian
|
|
LANGUAGE_NORWEGIAN -> R.xml.keys_letters_norwegian
|
|
LANGUAGE_POLISH -> R.xml.keys_letters_polish
|
|
LANGUAGE_ROMANIAN -> R.xml.keys_letters_romanian
|
|
LANGUAGE_RUSSIAN -> R.xml.keys_letters_russian
|
|
LANGUAGE_SLOVENIAN -> R.xml.keys_letters_slovenian
|
|
LANGUAGE_SWEDISH -> R.xml.keys_letters_swedish
|
|
LANGUAGE_SPANISH -> R.xml.keys_letters_spanish_qwerty
|
|
LANGUAGE_TURKISH_Q -> R.xml.keys_letters_turkish_q
|
|
else -> R.xml.keys_letters_english_qwerty
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
@SuppressLint("RestrictedApi", "UseCompatLoadingForDrawables")
|
|
private fun buildSuggestionTextStyle(): Bundle {
|
|
val stylesBuilder = UiVersions.newStylesBuilder()
|
|
|
|
val verticalPadding = resources.getDimensionPixelSize(R.dimen.small_margin)
|
|
val horizontalPadding = resources.getDimensionPixelSize(R.dimen.activity_margin)
|
|
|
|
val textSize = resources.getDimension(R.dimen.label_text_size) / resources.displayMetrics.scaledDensity
|
|
|
|
val rippleBg = resources.getDrawable(R.drawable.clipboard_background, theme) as RippleDrawable
|
|
val layerDrawable = rippleBg.findDrawableByLayerId(R.id.clipboard_background_holder) as LayerDrawable
|
|
layerDrawable.findDrawableByLayerId(R.id.clipboard_background_stroke).applyColorFilter(getStrokeColor())
|
|
layerDrawable.findDrawableByLayerId(R.id.clipboard_background_shape).applyColorFilter(getProperBackgroundColor())
|
|
|
|
val maxWidth = resources.getDimensionPixelSize(R.dimen.suggestion_max_width)
|
|
val height = resources.getDimensionPixelSize(R.dimen.label_text_size) + verticalPadding * 2
|
|
|
|
val chipStyle =
|
|
ViewStyle.Builder()
|
|
.setBackground(Icon.createWithBitmap(rippleBg.toBitmap(width = maxWidth, height = height)))
|
|
.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
|
|
.build()
|
|
|
|
val iconStyle = ImageViewStyle.Builder().build()
|
|
|
|
val style = InlineSuggestionUi.newStyleBuilder()
|
|
.setSingleIconChipStyle(chipStyle)
|
|
.setChipStyle(chipStyle)
|
|
.setStartIconStyle(iconStyle)
|
|
.setEndIconStyle(iconStyle)
|
|
.setSingleIconChipIconStyle(iconStyle)
|
|
.setTitleStyle(
|
|
TextViewStyle.Builder()
|
|
.setLayoutMargin(0, 0, horizontalPadding, 0)
|
|
.setTextColor(getProperTextColor())
|
|
.setTextSize(textSize)
|
|
.build()
|
|
)
|
|
.setSubtitleStyle(
|
|
TextViewStyle.Builder()
|
|
.setTextColor(getProperTextColor())
|
|
.setTextSize(textSize)
|
|
.build()
|
|
)
|
|
.build()
|
|
stylesBuilder.addStyle(style)
|
|
return stylesBuilder.build()
|
|
}
|
|
|
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
|
keyboardView?.setupKeyboard()
|
|
}
|
|
}
|