From 79d476fe39f20f92185957e594bb65a91ff70bcb Mon Sep 17 00:00:00 2001 From: danilodominguezperez Date: Mon, 13 Nov 2023 09:01:53 -0500 Subject: [PATCH] Add back SamsungInputConnection --- .../kotlin/org/wordpress/aztec/AztecText.kt | 8 +- .../wordpress/aztec/SamsungInputConnection.kt | 187 ++++++++++++++++++ 2 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 aztec/src/main/kotlin/org/wordpress/aztec/SamsungInputConnection.kt diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt index 51ea796fe..a5d97bfec 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt @@ -698,12 +698,14 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection { val inputConnection = requireNotNull(super.onCreateInputConnection(outAttrs)).wrapWithBackSpaceHandler() - if (shouldOverridePredictiveTextBehavior()) { + return if (shouldOverridePredictiveTextBehavior()) { AppLog.d(AppLog.T.EDITOR, "Disabling autocorrect on Samsung device with Samsung Keyboard with API 33") outAttrs.inputType = outAttrs.inputType or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - } - return inputConnection + SamsungInputConnection(this, inputConnection) + } else { + inputConnection + } } private fun InputConnection.wrapWithBackSpaceHandler(): InputConnection { diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/SamsungInputConnection.kt b/aztec/src/main/kotlin/org/wordpress/aztec/SamsungInputConnection.kt new file mode 100644 index 000000000..01b8f3dbd --- /dev/null +++ b/aztec/src/main/kotlin/org/wordpress/aztec/SamsungInputConnection.kt @@ -0,0 +1,187 @@ +package org.wordpress.aztec + +import android.os.Build +import android.os.Bundle +import android.text.Editable +import android.text.Selection +import android.text.Spanned +import android.text.style.SuggestionSpan +import android.view.KeyEvent +import android.view.inputmethod.BaseInputConnection +import android.view.inputmethod.CompletionInfo +import android.view.inputmethod.CorrectionInfo +import android.view.inputmethod.ExtractedText +import android.view.inputmethod.ExtractedTextRequest +import android.view.inputmethod.InputConnection +import android.view.inputmethod.InputContentInfo + +/** + * Wrapper around proprietary Samsung InputConnection. Forwards all the calls to it, except for getExtractedText and + * some custom logic in commitText + */ +class SamsungInputConnection( + private val mTextView: AztecText, + private val baseInputConnection: InputConnection +) : BaseInputConnection(mTextView, true) { + + override fun getEditable(): Editable { + return mTextView.editableText + } + + override fun beginBatchEdit(): Boolean { + return baseInputConnection.beginBatchEdit() + } + + override fun endBatchEdit(): Boolean { + return baseInputConnection.endBatchEdit() + } + + override fun clearMetaKeyStates(states: Int): Boolean { + return baseInputConnection.clearMetaKeyStates(states) + } + + override fun sendKeyEvent(event: KeyEvent?): Boolean { + return super.sendKeyEvent(event) + } + + override fun commitCompletion(text: CompletionInfo?): Boolean { + return baseInputConnection.commitCompletion(text) + } + + override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean { + return baseInputConnection.commitCorrection(correctionInfo) + } + + override fun performEditorAction(actionCode: Int): Boolean { + return baseInputConnection.performEditorAction(actionCode) + } + + override fun performContextMenuAction(id: Int): Boolean { + return baseInputConnection.performContextMenuAction(id) + } + + // Extracted text on Samsung devices on Android 13 is somehow used for Grammarly suggestions which causes a lot of + // issues with spans and cursors. We do not use extracted text, so returning null + // (default behavior of BaseInputConnection) prevents Grammarly from messing up content most of the time + override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText? { + return null + } + + override fun performPrivateCommand(action: String?, data: Bundle?): Boolean { + return baseInputConnection.performPrivateCommand(action, data) + } + + override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean { + return baseInputConnection.setComposingText(text, newCursorPosition) + } + + override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean { + val incomingTextHasSuggestions = text is Spanned && + text.getSpans(0, text.length, SuggestionSpan::class.java).isNotEmpty() + + // Sometime spellchecker tries to commit partial text with suggestions. This mostly works ok, + // but Aztec spans are finicky, and tend to get messed when content of the editor is replaced. + // In this method we do everything replaceText method of EditableInputConnection does, apart from actually + // replacing text. Instead we copy the suggestions from incoming text into editor directly. + if (incomingTextHasSuggestions) { + // delete composing text set previously. + var composingSpanStart = getComposingSpanStart(editable) + var composingSpanEnd = getComposingSpanEnd(editable) + + if (composingSpanEnd < composingSpanStart) { + val tmp = composingSpanStart + composingSpanStart = composingSpanEnd + composingSpanEnd = tmp + } + + if (composingSpanStart != -1 && composingSpanEnd != -1) { + removeComposingSpans(editable) + } else { + composingSpanStart = Selection.getSelectionStart(editable) + composingSpanEnd = Selection.getSelectionEnd(editable) + if (composingSpanStart < 0) composingSpanStart = 0 + if (composingSpanEnd < 0) composingSpanEnd = 0 + if (composingSpanEnd < composingSpanStart) { + val tmp = composingSpanStart + composingSpanStart = composingSpanEnd + composingSpanEnd = tmp + } + } + + var cursorPosition = newCursorPosition + cursorPosition += if (cursorPosition > 0) { + composingSpanEnd - 1 + } else { + composingSpanStart + } + if (newCursorPosition < 0) cursorPosition = 0 + if (newCursorPosition > editable.length) cursorPosition = editable.length + Selection.setSelection(editable, cursorPosition) + + (text as Spanned).getSpans(0, text.length, SuggestionSpan::class.java).forEach { + val st: Int = text.getSpanStart(it) + val en: Int = text.getSpanEnd(it) + val fl: Int = text.getSpanFlags(it) + + if (editable.length > composingSpanStart + en) { + editable.setSpan(it, composingSpanStart + st, composingSpanStart + en, fl) + } + } + + return true + } + return baseInputConnection.commitText(text, newCursorPosition) + } + + override fun commitContent(inputContentInfo: InputContentInfo, flags: Int, opts: Bundle?): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + baseInputConnection.commitContent(inputContentInfo, flags, opts) + } else { + super.commitContent(inputContentInfo, flags, opts) + } + } + + override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean { + return baseInputConnection.deleteSurroundingText(beforeLength, afterLength) + } + + override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean { + return baseInputConnection.requestCursorUpdates(cursorUpdateMode) + } + + override fun reportFullscreenMode(enabled: Boolean): Boolean { + return baseInputConnection.reportFullscreenMode(enabled) + } + + override fun setSelection(start: Int, end: Int): Boolean { + return baseInputConnection.setSelection(start, end) + } + + override fun finishComposingText(): Boolean { + return baseInputConnection.finishComposingText() + } + + override fun setComposingRegion(start: Int, end: Int): Boolean { + return baseInputConnection.setComposingRegion(start, end) + } + + override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean { + return baseInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength) + } + + override fun getCursorCapsMode(reqModes: Int): Int { + return baseInputConnection.getCursorCapsMode(reqModes) + } + + override fun getSelectedText(flags: Int): CharSequence? { + return baseInputConnection.getSelectedText(flags) + } + + override fun getTextAfterCursor(length: Int, flags: Int): CharSequence? { + return baseInputConnection.getTextAfterCursor(length, flags) + } + + override fun getTextBeforeCursor(length: Int, flags: Int): CharSequence? { + return baseInputConnection.getTextBeforeCursor(length, flags) + } +}