Skip to content

Commit 4a6a062

Browse files
authored
Merge pull request #1024 from wordpress-mobile/issue/1023-fix-predictive-text-issue-on-samsung-devices
Fix predictive text issue with Samsung devices on Android 13
2 parents 9b58f93 + 0253f5a commit 4a6a062

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,8 @@ open class MainActivity : AppCompatActivity(),
395395
val sourceEditor = findViewById<SourceViewEditText>(R.id.source)
396396
val toolbar = findViewById<AztecToolbar>(R.id.formatting_toolbar)
397397

398+
visualEditor.enableSamsungPredictiveBehaviorOverride()
399+
398400
visualEditor.externalLogger = object : AztecLog.ExternalLogger {
399401
override fun log(message: String) {
400402
}

aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import android.view.View
5050
import android.view.View.OnLongClickListener
5151
import android.view.WindowManager
5252
import android.view.inputmethod.BaseInputConnection
53+
import android.view.inputmethod.EditorInfo
54+
import android.view.inputmethod.InputConnection
5355
import android.widget.CheckBox
5456
import android.widget.EditText
5557
import android.widget.Toast
@@ -236,6 +238,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
236238
private var bypassObservationQueue: Boolean = false
237239
private var bypassMediaDeletedListener: Boolean = false
238240
private var bypassCrashPreventerInputFilter: Boolean = false
241+
private var overrideSamsungPredictiveBehavior: Boolean = false
239242

240243
var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)
241244

@@ -654,6 +657,17 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
654657
return super.onTouchEvent(event)
655658
}
656659

660+
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
661+
val baseInputConnection = requireNotNull(super.onCreateInputConnection(outAttrs))
662+
return if (Build.MANUFACTURER.lowercase(Locale.US) == "samsung" && Build.VERSION.SDK_INT == 33
663+
&& overrideSamsungPredictiveBehavior) {
664+
AppLog.d(AppLog.T.EDITOR, "Overriding predictive text behavior on Samsung device with API 33")
665+
SamsungInputConnection(this, baseInputConnection)
666+
} else {
667+
baseInputConnection
668+
}
669+
}
670+
657671
// Setup the keyListener(s) for Backspace and Enter key.
658672
// Backspace: If listener does return false we remove the style here
659673
// Enter: Ask the listener if we need to insert or not the char
@@ -1713,6 +1727,13 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
17131727
bypassMediaDeletedListener = false
17141728
}
17151729

1730+
// removes Grammarly suggestions from default keyboard on Samsung devices on Android 13 (API 33)
1731+
// Grammarly implementation is often messing spans and cursor position, as described here:
1732+
// https://github.com/wordpress-mobile/AztecEditor-Android/issues/1023
1733+
fun enableSamsungPredictiveBehaviorOverride() {
1734+
overrideSamsungPredictiveBehavior = true
1735+
}
1736+
17161737
fun isMediaDeletedListenerDisabled(): Boolean {
17171738
return bypassMediaDeletedListener
17181739
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package org.wordpress.aztec
2+
3+
import android.os.Build
4+
import android.os.Bundle
5+
import android.text.Editable
6+
import android.text.Selection
7+
import android.text.Spanned
8+
import android.text.TextUtils
9+
import android.text.style.SuggestionSpan
10+
import android.view.KeyEvent
11+
import android.view.inputmethod.BaseInputConnection
12+
import android.view.inputmethod.CompletionInfo
13+
import android.view.inputmethod.CorrectionInfo
14+
import android.view.inputmethod.ExtractedText
15+
import android.view.inputmethod.ExtractedTextRequest
16+
import android.view.inputmethod.InputConnection
17+
import android.view.inputmethod.InputContentInfo
18+
19+
/**
20+
* Wrapper around proprietary Samsung InputConnection. Forwards all the calls to it, except for getExtractedText and
21+
* some custom logic in commitText
22+
*/
23+
class SamsungInputConnection(
24+
private val mTextView: AztecText,
25+
private val baseInputConnection: InputConnection,
26+
) : BaseInputConnection(mTextView, true) {
27+
28+
override fun getEditable(): Editable {
29+
return mTextView.editableText
30+
}
31+
32+
override fun beginBatchEdit(): Boolean {
33+
return baseInputConnection.beginBatchEdit()
34+
}
35+
36+
override fun endBatchEdit(): Boolean {
37+
return baseInputConnection.endBatchEdit()
38+
}
39+
40+
override fun clearMetaKeyStates(states: Int): Boolean {
41+
return baseInputConnection.clearMetaKeyStates(states)
42+
}
43+
44+
override fun sendKeyEvent(event: KeyEvent?): Boolean {
45+
return super.sendKeyEvent(event)
46+
}
47+
48+
override fun commitCompletion(text: CompletionInfo?): Boolean {
49+
return baseInputConnection.commitCompletion(text)
50+
}
51+
52+
override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean {
53+
return baseInputConnection.commitCorrection(correctionInfo)
54+
}
55+
56+
override fun performEditorAction(actionCode: Int): Boolean {
57+
return baseInputConnection.performEditorAction(actionCode)
58+
}
59+
60+
override fun performContextMenuAction(id: Int): Boolean {
61+
return baseInputConnection.performContextMenuAction(id)
62+
}
63+
64+
// Extracted text on Samsung devices on Android 13 is somehow used for Grammarly suggestions which causes a lot of
65+
// issues with spans and cursors. We do not use extracted text, so returning null
66+
// (default behavior of BaseInputConnection) prevents Grammarly from messing up content most of the time
67+
override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText? {
68+
return null
69+
}
70+
71+
override fun performPrivateCommand(action: String?, data: Bundle?): Boolean {
72+
return baseInputConnection.performPrivateCommand(action, data)
73+
}
74+
75+
override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
76+
return baseInputConnection.setComposingText(text, newCursorPosition)
77+
}
78+
79+
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
80+
val incomingTextHasSuggestions = text is Spanned &&
81+
text.getSpans(0, text.length, SuggestionSpan::class.java).isNotEmpty()
82+
83+
// Sometime spellchecker tries to commit partial text with suggestions. This mostly works ok,
84+
// but Aztec spans are finicky, and tend to get messed when content of the editor is replaced.
85+
// In this method we do everything replaceText method of EditableInputConnection does, apart from actually
86+
// replacing text. Instead we copy the suggestions from incoming text into editor directly.
87+
if (incomingTextHasSuggestions) {
88+
// delete composing text set previously.
89+
var composingSpanStart = getComposingSpanStart(editable)
90+
var composingSpanEnd = getComposingSpanEnd(editable)
91+
92+
if (composingSpanEnd < composingSpanStart) {
93+
val tmp = composingSpanStart
94+
composingSpanStart = composingSpanEnd
95+
composingSpanEnd = tmp
96+
}
97+
98+
if (composingSpanStart != -1 && composingSpanEnd != -1) {
99+
removeComposingSpans(editable)
100+
} else {
101+
composingSpanStart = Selection.getSelectionStart(editable)
102+
composingSpanEnd = Selection.getSelectionEnd(editable)
103+
if (composingSpanStart < 0) composingSpanStart = 0
104+
if (composingSpanEnd < 0) composingSpanEnd = 0
105+
if (composingSpanEnd < composingSpanStart) {
106+
val tmp = composingSpanStart
107+
composingSpanStart = composingSpanEnd
108+
composingSpanEnd = tmp
109+
}
110+
}
111+
112+
var cursorPosition = newCursorPosition
113+
cursorPosition += if (cursorPosition > 0) {
114+
composingSpanEnd - 1
115+
} else {
116+
composingSpanStart
117+
}
118+
if (newCursorPosition < 0) cursorPosition = 0
119+
if (newCursorPosition > editable.length) cursorPosition = editable.length
120+
Selection.setSelection(editable, cursorPosition)
121+
122+
(text as Spanned).getSpans(0, text.length, SuggestionSpan::class.java).forEach {
123+
val st: Int = text.getSpanStart(it)
124+
val en: Int = text.getSpanEnd(it)
125+
val fl: Int = text.getSpanFlags(it)
126+
127+
if (editable.length > composingSpanStart + en) {
128+
editable.setSpan(it, composingSpanStart + st, composingSpanStart + en, fl)
129+
}
130+
}
131+
132+
return true
133+
}
134+
return baseInputConnection.commitText(text, newCursorPosition)
135+
}
136+
137+
override fun commitContent(inputContentInfo: InputContentInfo, flags: Int, opts: Bundle?): Boolean {
138+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
139+
baseInputConnection.commitContent(inputContentInfo, flags, opts)
140+
} else {
141+
super.commitContent(inputContentInfo, flags, opts)
142+
}
143+
}
144+
145+
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
146+
return baseInputConnection.deleteSurroundingText(beforeLength, afterLength)
147+
}
148+
149+
override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
150+
return baseInputConnection.requestCursorUpdates(cursorUpdateMode)
151+
}
152+
153+
override fun reportFullscreenMode(enabled: Boolean): Boolean {
154+
return baseInputConnection.reportFullscreenMode(enabled)
155+
}
156+
157+
override fun setSelection(start: Int, end: Int): Boolean {
158+
return baseInputConnection.setSelection(start, end)
159+
}
160+
161+
override fun finishComposingText(): Boolean {
162+
return baseInputConnection.finishComposingText()
163+
}
164+
165+
override fun setComposingRegion(start: Int, end: Int): Boolean {
166+
return baseInputConnection.setComposingRegion(start, end)
167+
}
168+
169+
override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
170+
return baseInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
171+
}
172+
173+
override fun getCursorCapsMode(reqModes: Int): Int {
174+
return baseInputConnection.getCursorCapsMode(reqModes)
175+
}
176+
177+
override fun getSelectedText(flags: Int): CharSequence? {
178+
return baseInputConnection.getSelectedText(flags)
179+
}
180+
181+
override fun getTextAfterCursor(length: Int, flags: Int): CharSequence {
182+
return baseInputConnection.getTextAfterCursor(length, flags)
183+
}
184+
185+
override fun getTextBeforeCursor(length: Int, flags: Int): CharSequence {
186+
return baseInputConnection.getTextBeforeCursor(length, flags)
187+
}
188+
}

0 commit comments

Comments
 (0)