SDL: Android: use real editable text and mimic the edit operations to generate key events

From 82e341bc9e914e753fe7842834d37b16aebc4ad5 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 30 Sep 2022 11:40:29 -0700
Subject: [PATCH] Android: use real editable text and mimic the edit operations
 to generate key events

This fixes issues where the IME and the output would get out of sync
---
 Android.mk                                    |   0
 .../main/java/org/libsdl/app/SDLActivity.java | 174 +++++++++++++-----
 2 files changed, 133 insertions(+), 41 deletions(-)
 mode change 100644 => 100755 Android.mk

diff --git a/Android.mk b/Android.mk
old mode 100644
new mode 100755
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index 8d4039c1ba52..c9019dad6c2a 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -44,6 +44,7 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Button;
+import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
@@ -1874,9 +1875,17 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
 
 class SDLInputConnection extends BaseInputConnection {
 
+    protected EditText mEditText;
+    protected int m_nLastContentLength = 0;
+
     public SDLInputConnection(View targetView, boolean fullEditor) {
         super(targetView, fullEditor);
+        mEditText = new EditText(SDL.getContext());
+    }
 
+    @Override
+    public Editable getEditable() {
+        return mEditText.getEditableText();
     }
 
     @Override
@@ -1899,57 +1908,154 @@ public boolean sendKeyEvent(KeyEvent event) {
             }
         }
 
-
         return super.sendKeyEvent(event);
     }
 
     @Override
     public boolean commitText(CharSequence text, int newCursorPosition) {
+        replaceText(text, newCursorPosition, false);
+
+        return super.commitText(text, newCursorPosition);
+    }
+
+    @Override
+    public boolean setComposingText(CharSequence text, int newCursorPosition) {
+        replaceText(text, newCursorPosition, true);
 
-        /* Generate backspaces for the text we're going to replace */
+        return super.setComposingText(text, newCursorPosition);
+    }
+
+    @Override
+    public boolean setComposingRegion(int start, int end) {
         final Editable content = getEditable();
         if (content != null) {
-            int a = getComposingSpanStart(content);
-            int b = getComposingSpanEnd(content);
-            if (a == -1 || b == -1) {
-                a = Selection.getSelectionStart(content);
-                b = Selection.getSelectionEnd(content);
-            }
-            if (a < 0) a = 0;
-            if (b < 0) b = 0;
-            if (b < a) {
+            int a = start;
+            int b = end;
+            if (a > b) {
                 int tmp = a;
                 a = b;
                 b = tmp;
             }
-            int backspaces = (b - a);
 
-            for (int i = 0; i < backspaces; i++) {
-                nativeGenerateScancodeForUnichar('\b');
+            // Clip the end points to be within the content bounds.
+            final int length = content.length();
+            if (a < 0) {
+                a = 0;
+            }
+            if (b < 0) {
+                b = 0;
+            }
+            if (a > length) {
+                a = length;
             }
+            if (b > length) {
+                b = length;
+            }
+
+            deleteText(a, b);
         }
 
-        for (int i = 0; i < text.length(); i++) {
-            char c = text.charAt(i);
-            if (c == '\n') {
-                if (SDLActivity.onNativeSoftReturnKey()) {
-                    return true;
+        return super.setComposingRegion(start, end);
+    }
+
+    @Override
+    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+        final Editable content = getEditable();
+        if (content != null) {
+            int a = Selection.getSelectionStart(content);
+            int b = Selection.getSelectionEnd(content);
+
+            if (a > b) {
+                int tmp = a;
+                a = b;
+                b = tmp;
+            }
+
+            // ignore the composing text.
+            int ca = getComposingSpanStart(content);
+            int cb = getComposingSpanEnd(content);
+            if (cb < ca) {
+                int tmp = ca;
+                ca = cb;
+                cb = tmp;
+            }
+
+            if (ca != -1 && cb != -1) {
+                if (ca < a) {
+                    a = ca;
+                }
+                if (cb > b) {
+                    b = cb;
                 }
             }
-            nativeGenerateScancodeForUnichar(c);
-        }
 
-        SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
+            if (beforeLength > 0) {
+                int start = a - beforeLength;
+                if (start < 0) {
+                    start = 0;
+                }
+                deleteText(start, a);
+            }
+        }
 
-        return super.commitText(text, newCursorPosition);
+        return super.deleteSurroundingText(beforeLength, afterLength);
     }
 
-    @Override
-    public boolean setComposingText(CharSequence text, int newCursorPosition) {
+    protected void replaceText(CharSequence text, int newCursorPosition, boolean composing) {
+        final Editable content = getEditable();
+        if (content == null) {
+            return;
+        }
+        
+        // delete composing text set previously.
+        int a = getComposingSpanStart(content);
+        int b = getComposingSpanEnd(content);
+
+        if (b < a) {
+            int tmp = a;
+            a = b;
+            b = tmp;
+        }
+        if (a == -1 || b == -1) {
+            a = Selection.getSelectionStart(content);
+            b = Selection.getSelectionEnd(content);
+            if (a < 0) {
+                a = 0;
+            }
+            if (b < 0) {
+                b = 0;
+            }
+            if (b < a) {
+                int tmp = a;
+                a = b;
+                b = tmp;
+            }
+        }
 
-        nativeSetComposingText(text.toString(), newCursorPosition);
+        deleteText(a, b);
 
-        return super.setComposingText(text, newCursorPosition);
+        if (composing) {
+            nativeSetComposingText(text.toString(), newCursorPosition);
+        } else {
+            for (int i = 0; i < text.length(); i++) {
+                char c = text.charAt(i);
+                if (c == '\n') {
+                    if (SDLActivity.onNativeSoftReturnKey()) {
+                        return;
+                    }
+                }
+                ++m_nLastContentLength;
+                nativeGenerateScancodeForUnichar(c);
+            }
+            SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
+        }
+    }
+
+    protected void deleteText(int start, int end) {
+        while (m_nLastContentLength > start) {
+            --m_nLastContentLength;
+            nativeGenerateScancodeForUnichar('\b');
+        }
     }
 
     public static native void nativeCommitText(String text, int newCursorPosition);
@@ -1958,20 +2064,6 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) {
 
     public native void nativeSetComposingText(String text, int newCursorPosition);
 
-    @Override
-    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
-        // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
-        // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
-        if (beforeLength > 0 && afterLength == 0) {
-            // backspace(s)
-            while (beforeLength-- > 0) {
-                nativeGenerateScancodeForUnichar('\b');
-            }
-            return true;
-        }
-
-        return super.deleteSurroundingText(beforeLength, afterLength);
-    }
 }
 
 class SDLClipboardHandler implements