Browse Source

refactor android input connection (#16490)

Emmanuel Hansen 1 year ago
parent
commit
20fe710681

+ 39 - 62
src/Android/Avalonia.Android/AndroidInputMethod.cs → src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs

@@ -5,10 +5,9 @@ using Android.Runtime;
 using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
-using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Input.TextInput;
 
-namespace Avalonia.Android
+namespace Avalonia.Android.Platform.Input
 {
     internal interface IAndroidInputMethod
     {
@@ -21,18 +20,18 @@ namespace Avalonia.Android
 
         public InputMethodManager IMM { get; }
 
-        void OnBatchEditedEnded();
+        void OnBatchEditEnded();
     }
 
     enum CustomImeFlags
-    { 
+    {
         ActionNone = 0x00000001,
-       ActionGo = 0x00000002,
-       ActionSearch = 0x00000003,
-       ActionSend = 0x00000004,
-       ActionNext = 0x00000005,
-       ActionDone = 0x00000006,
-       ActionPrevious = 0x00000007,
+        ActionGo = 0x00000002,
+        ActionSearch = 0x00000003,
+        ActionSend = 0x00000004,
+        ActionNext = 0x00000005,
+        ActionDone = 0x00000006,
+        ActionPrevious = 0x00000007,
     }
 
     internal class AndroidInputMethod<TView> : ITextInputMethodImpl, IAndroidInputMethod
@@ -79,7 +78,7 @@ namespace Avalonia.Android
             {
                 _host.RequestFocus();
 
-                _imm.RestartInput(View);              
+                _imm.RestartInput(View);
 
                 _imm.ShowSoftInput(_host, ShowFlags.Implicit);
 
@@ -110,27 +109,29 @@ namespace Avalonia.Android
 
         private void _client_SelectionChanged(object? sender, EventArgs e)
         {
-            if (_inputConnection is null || _inputConnection.IsInBatchEdit)
+            if (_inputConnection is null || _inputConnection.IsInBatchEdit || _inputConnection.IsInUpdate)
                 return;
             OnSelectionChanged();
         }
 
         private void OnSelectionChanged()
         {
-            if (Client is null || _inputConnection is null)
+            if (Client is null || _inputConnection is null || _inputConnection.IsInUpdate)
             {
                 return;
             }
 
             OnSurroundingTextChanged();
 
-            var selection = Client.Selection;
+            _inputConnection.IsInUpdate = true;
 
-            _inputConnection.SetSelection(selection.Start, selection.End);
+            var selection = Client.Selection;
 
-            var composition = _inputConnection.EditableWrapper.CurrentComposition;
+            var composition = _inputConnection.EditBuffer.HasComposition ? _inputConnection.EditBuffer.Composition!.Value : new TextSelection(-1,-1);
 
             _imm.UpdateSelection(_host, selection.Start, selection.End, composition.Start, composition.End);
+
+            _inputConnection.IsInUpdate = false;
         }
 
         private void _client_SurroundingTextChanged(object? sender, EventArgs e)
@@ -140,7 +141,7 @@ namespace Avalonia.Android
             OnSurroundingTextChanged();
         }
 
-        public void OnBatchEditedEnded()
+        public void OnBatchEditEnded()
         {
             if (_inputConnection is null || _inputConnection.IsInBatchEdit)
                 return;
@@ -149,56 +150,32 @@ namespace Avalonia.Android
 
         private void OnSurroundingTextChanged()
         {
-            if(_client is null || _inputConnection is null)
+            if (_client is null || _inputConnection is null || _inputConnection.IsInUpdate)
             {
                 return;
             }
 
-            var surroundingText = _client.SurroundingText ?? "";
-            var editableText = _inputConnection.EditableWrapper.ToString();
-
-            if (editableText != surroundingText)
+            if (_inputConnection.IsInMonitorMode)
             {
-                _inputConnection.EditableWrapper.IgnoreChange = true;
-
-                var diff = GetDiff();
-
-                _inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff);
-
-                _inputConnection.EditableWrapper.IgnoreChange = false;
-
-                if(diff.index == 0)
-                {
-                    var selection = _client.Selection;
-                    _client.Selection = new TextSelection(selection.Start, 0);
-                    _client.Selection = selection;
-                }
-            }
-
-            (int index, string diff) GetDiff()
-            {
-                int index = 0;
+                var surroundingText = _client.SurroundingText ?? "";
 
-                var longerLength = Math.Max(surroundingText.Length, editableText.Length);
+                var selection = _client.Selection;
 
-                for (int i = 0; i < longerLength; i++)
+                var extractedText = new ExtractedText
                 {
-                    if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i])
-                    {
-                        index = i;
-                        break;
-                    }
-                }
-
-                var diffString = surroundingText.Substring(index, surroundingText.Length - index);
+                    Text = new Java.Lang.String(surroundingText),
+                    SelectionStart = selection.Start,
+                    SelectionEnd = selection.End,
+                    PartialEndOffset = surroundingText.Length
+                };
 
-                return (index, diffString);
+                _imm.UpdateExtractedText(_host, _inputConnection.ExtractedTextToken, extractedText);
             }
         }
 
         public void SetCursorRect(Rect rect)
         {
-            
+
         }
 
         public void SetOptions(TextInputOptions options)
@@ -212,22 +189,22 @@ namespace Avalonia.Android
 
                 outAttrs.InputType = options.ContentType switch
                 {
-                    TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress,
-                    TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber,
-                    TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword,
-                    TextInputContentType.Digits => global::Android.Text.InputTypes.ClassPhone,
-                    TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri,
-                    _ => global::Android.Text.InputTypes.ClassText
+                    TextInputContentType.Email => InputTypes.TextVariationEmailAddress,
+                    TextInputContentType.Number => InputTypes.ClassNumber,
+                    TextInputContentType.Password => InputTypes.TextVariationPassword,
+                    TextInputContentType.Digits => InputTypes.ClassPhone,
+                    TextInputContentType.Url => InputTypes.TextVariationUri,
+                    _ => InputTypes.ClassText
                 };
 
                 if (options.AutoCapitalization)
                 {
-                    outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences;
-                    outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences;
+                    outAttrs.InitialCapsMode = CapitalizationMode.Sentences;
+                    outAttrs.InputType |= InputTypes.TextFlagCapSentences;
                 }
 
                 if (options.Multiline)
-                    outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine;
+                    outAttrs.InputType |= InputTypes.TextFlagMultiLine;
 
                 outAttrs.ImeOptions = options.ReturnKeyType switch
                 {

+ 310 - 0
src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs

@@ -0,0 +1,310 @@
+using System.Collections.Concurrent;
+using System.Threading;
+using Android.OS;
+using Android.Runtime;
+using Android.Text;
+using Android.Views;
+using Android.Views.InputMethods;
+using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Input;
+using Avalonia.Input.TextInput;
+using Java.Lang;
+
+namespace Avalonia.Android.Platform.Input
+{
+    internal class AvaloniaInputConnection : Object, IInputConnection
+    {
+        private readonly TopLevelImpl _toplevel;
+        private readonly IAndroidInputMethod _inputMethod;
+        private readonly TextEditBuffer _editBuffer;
+        private readonly ConcurrentQueue<EditCommand> _commandQueue;
+
+        private int _batchLevel = 0;
+
+        public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod)
+        {
+            _toplevel = toplevel;
+            _inputMethod = inputMethod;
+            _editBuffer = new TextEditBuffer(_inputMethod, toplevel);
+            _commandQueue = new ConcurrentQueue<EditCommand>();
+        }
+
+        public int ExtractedTextToken { get; private set; }
+
+        public IAndroidInputMethod InputMethod => _inputMethod;
+
+        public TopLevelImpl Toplevel => _toplevel;
+
+        public bool IsInBatchEdit => _batchLevel > 0;
+        public bool IsInMonitorMode { get; private set; }
+
+        public Handler? Handler => null;
+
+        public TextEditBuffer EditBuffer => _editBuffer;
+
+        public bool IsInUpdate { get; set; }
+
+        public bool SetComposingRegion(int start, int end)
+        {
+            if (InputMethod.IsActive)
+            {
+                QueueCommand(new CompositionRegionCommand(start, end));
+            }
+            return InputMethod.IsActive;
+        }
+
+        public bool SetComposingText(ICharSequence? text, int newCursorPosition)
+        {
+            if (text is null)
+            {
+                return false;
+            }
+
+            if (InputMethod.IsActive)
+            {
+                var compositionText = text.SubSequence(0, text.Length());
+                QueueCommand(new CompositionTextCommand(compositionText, newCursorPosition));
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public bool SetSelection(int start, int end)
+        {
+            if (InputMethod.IsActive)
+            {
+                if (IsInUpdate)
+                    new SelectionCommand(start, end).Apply(EditBuffer);
+                else
+                    QueueCommand(new SelectionCommand(start, end));
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public bool BeginBatchEdit()
+        {
+            _batchLevel = Interlocked.Increment(ref _batchLevel);
+            return InputMethod.IsActive;
+        }
+
+        public bool EndBatchEdit()
+        {
+            _batchLevel = Interlocked.Decrement(ref _batchLevel);
+
+            if (!IsInBatchEdit)
+            {
+                IsInUpdate = true;
+                while (_commandQueue.TryDequeue(out var command))
+                {
+                    command.Apply(_editBuffer);
+                }
+                IsInUpdate = false;
+            }
+            return IsInBatchEdit;
+        }
+
+        public bool CommitText(ICharSequence? text, int newCursorPosition)
+        {
+            if (InputMethod.Client is null || text is null)
+            {
+                return false;
+            }
+
+            if (InputMethod.IsActive)
+            {
+                var committedText = text.SubSequence(0, text.Length());
+                QueueCommand(new CommitTextCommand(committedText, newCursorPosition));
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public bool DeleteSurroundingText(int beforeLength, int afterLength)
+        {
+            if (InputMethod.IsActive)
+            {
+                QueueCommand(new DeleteRegionCommand(beforeLength, afterLength));
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
+        {
+            switch (actionCode)
+            {
+                case ImeAction.Done:
+                    {
+                        _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly);
+                        break;
+                    }
+                case ImeAction.Next:
+                    {
+                        FocusManager.GetFocusManager(_toplevel.InputRoot)?
+                            .TryMoveFocus(NavigationDirection.Next);
+                        break;
+                    }
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags)
+        {
+            IsInMonitorMode = ((int)flags & (int)TextExtractFlags.Monitor) != 0;
+
+            ExtractedTextToken = IsInMonitorMode ? request?.Token ?? 0 : ExtractedTextToken;
+
+            if (!_inputMethod.IsActive)
+            {
+                return null;
+            }
+
+            var extract = new ExtractedText
+            {
+                Flags = 0,
+                PartialStartOffset = -1,
+                PartialEndOffset = -1,
+                SelectionStart = _editBuffer.Selection.Start,
+                SelectionEnd = _editBuffer.Selection.End,
+                StartOffset = 0
+            };
+
+            extract.Text = new SpannableString(_editBuffer.Text);
+
+            return extract;
+        }
+
+        public bool PerformContextMenuAction(int id)
+        {
+            if (InputMethod.Client is not { } client)
+                return false;
+
+            switch (id)
+            {
+                case global::Android.Resource.Id.SelectAll:
+                    client.ExecuteContextMenuAction(ContextMenuAction.SelectAll);
+                    return true;
+                case global::Android.Resource.Id.Cut:
+                    client.ExecuteContextMenuAction(ContextMenuAction.Cut);
+                    return true;
+                case global::Android.Resource.Id.Copy:
+                    client.ExecuteContextMenuAction(ContextMenuAction.Copy);
+                    return true;
+                case global::Android.Resource.Id.Paste:
+                    client.ExecuteContextMenuAction(ContextMenuAction.Paste);
+                    return true;
+                default:
+                    break;
+            }
+            return InputMethod.IsActive;
+        }
+
+        public bool ClearMetaKeyStates([GeneratedEnum] MetaKeyStates states)
+        {
+            return false;
+        }
+
+        public void CloseConnection()
+        {
+            _commandQueue.Clear();
+            _batchLevel = 0;
+        }
+
+        public bool CommitCompletion(CompletionInfo? text)
+        {
+            return false;
+        }
+
+        public bool CommitContent(InputContentInfo inputContentInfo, [GeneratedEnum] InputContentFlags flags, Bundle? opts)
+        {
+            return false;
+        }
+
+        public bool CommitCorrection(CorrectionInfo? correctionInfo)
+        {
+            return false;
+        }
+
+        public bool DeleteSurroundingTextInCodePoints(int beforeLength, int afterLength)
+        {
+            if (InputMethod.IsActive)
+            {
+                QueueCommand(new DeleteRegionInCodePointsCommand(beforeLength, afterLength));
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        public bool FinishComposingText()
+        {
+            if (InputMethod.IsActive)
+            {
+                QueueCommand(new FinishComposingCommand());
+            }
+
+            return InputMethod.IsActive;
+        }
+
+        [return: GeneratedEnum]
+        public CapitalizationMode GetCursorCapsMode([GeneratedEnum] CapitalizationMode reqModes)
+        {
+            return TextUtils.GetCapsMode(_editBuffer.Text, _editBuffer.Selection.Start, reqModes);
+        }
+
+        public ICharSequence? GetSelectedTextFormatted([GeneratedEnum] GetTextFlags flags)
+        {
+            return new SpannableString(_editBuffer.SelectedText);
+        }
+
+        public ICharSequence? GetTextAfterCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags)
+        {
+            var end = Math.Min(_editBuffer.Selection.End, _editBuffer.Text.Length);
+            return new SpannableString(_editBuffer.Text.Substring(end, Math.Min(n, _editBuffer.Text.Length - end)));
+        }
+
+        public ICharSequence? GetTextBeforeCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags)
+        {
+            var start = Math.Max(0, _editBuffer.Selection.Start - n);
+            var length = _editBuffer.Selection.Start - start;
+            return _editBuffer.Text == null ? null : new SpannableString(_editBuffer.Text.Substring(start, length));
+        }
+
+        public bool PerformPrivateCommand(string? action, Bundle? data)
+        {
+            return false;
+        }
+
+        public bool ReportFullscreenMode(bool enabled)
+        {
+            return false;
+        }
+
+        public bool RequestCursorUpdates(int cursorUpdateMode)
+        {
+            return false;
+        }
+
+        public bool SendKeyEvent(KeyEvent? e)
+        {
+            _inputMethod.View.DispatchKeyEvent(e);
+
+            return true;
+        }
+
+        private void QueueCommand(EditCommand command)
+        {
+            BeginBatchEdit();
+
+            try
+            {
+                _commandQueue.Enqueue(command);
+            }
+            finally
+            {
+                EndBatchEdit();
+            }
+        }
+    }
+}

+ 181 - 0
src/Android/Avalonia.Android/Platform/Input/EditCommand.cs

@@ -0,0 +1,181 @@
+using System;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.Android.Platform.Input
+{
+    internal abstract class EditCommand
+    {
+        public abstract void Apply(TextEditBuffer buffer);
+    }
+
+    internal class SelectionCommand : EditCommand
+    {
+        private readonly int _start;
+        private readonly int _end;
+
+        public SelectionCommand(int start, int end)
+        {
+            _start = Math.Min(start, end);
+            _end = Math.Max(start, end);
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            buffer.Selection = new TextSelection(Math.Max(_start, 0), Math.Min(_end, buffer.Text.Length));
+        }
+    }
+
+    internal class CompositionRegionCommand : EditCommand
+    {
+        private readonly int _start;
+        private readonly int _end;
+
+        public CompositionRegionCommand(int start, int end)
+        {
+            _start = Math.Min(start, end);
+            _end = Math.Max(start, end);
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            buffer.Composition = new TextSelection(_start, _end);
+        }
+    }
+
+    internal class DeleteRegionCommand : EditCommand
+    {
+        private readonly int _before;
+        private readonly int _after;
+
+        public DeleteRegionCommand(int before, int after)
+        {
+            _before = before;
+            _after = after;
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            var end = Math.Min(buffer.Text.Length, buffer.Selection.End + _after);
+            var endCount = end - buffer.Selection.End;
+            var start = Math.Max(0, buffer.Selection.Start - _before);
+            buffer.Remove(buffer.Selection.End, endCount);
+            buffer.Remove(start, buffer.Selection.Start - start);
+            buffer.Selection = new TextSelection(start, start);
+        }
+    }
+
+    internal class DeleteRegionInCodePointsCommand : EditCommand
+    {
+        private readonly int _before;
+        private readonly int _after;
+
+        public DeleteRegionInCodePointsCommand(int before, int after)
+        {
+            _before = before;
+            _after = after;
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            var beforeLengthInChar = 0;
+
+            for (int i = 0; i < _before; i++)
+            {
+                beforeLengthInChar++;
+                if (buffer.Selection.Start > beforeLengthInChar)
+                {
+                    var lead = buffer.Text[buffer.Selection.Start - beforeLengthInChar - 1];
+                    var trail = buffer.Text[buffer.Selection.Start - beforeLengthInChar];
+
+                    if (char.IsSurrogatePair(lead, trail))
+                    {
+                        beforeLengthInChar++;
+                    }
+                }
+
+                if (beforeLengthInChar == buffer.Selection.Start)
+                    break;
+            }
+
+            var afterLengthInChar = 0;
+            for (int i = 0; i < _after; i++)
+            {
+                afterLengthInChar++;
+                if (buffer.Selection.End > afterLengthInChar)
+                {
+                    var lead = buffer.Text[buffer.Selection.End + afterLengthInChar - 1];
+                    var trail = buffer.Text[buffer.Selection.End + afterLengthInChar];
+
+                    if (char.IsSurrogatePair(lead, trail))
+                    {
+                        afterLengthInChar++;
+                    }
+                }
+
+                if (buffer.Selection.End + afterLengthInChar == buffer.Text.Length)
+                    break;
+            }
+
+            var start = buffer.Selection.Start - beforeLengthInChar;
+            buffer.Remove(buffer.Selection.End, afterLengthInChar);
+            buffer.Remove(start, beforeLengthInChar);
+            buffer.Selection = new TextSelection(start, start);
+        }
+    }
+
+    internal class CompositionTextCommand : EditCommand
+    {
+        private readonly string _text;
+        private readonly int _newCursorPosition;
+
+        public CompositionTextCommand(string text, int newCursorPosition)
+        {
+            _text = text;
+            _newCursorPosition = newCursorPosition;
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            buffer.ComposingText = _text;
+            var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition;
+            buffer.Selection = new TextSelection(newCursor, newCursor);
+        }
+    }
+
+    internal class CommitTextCommand : EditCommand
+    {
+        private readonly string _text;
+        private readonly int _newCursorPosition;
+
+        public CommitTextCommand(string text, int newCursorPosition)
+        {
+            _text = text;
+            _newCursorPosition = newCursorPosition;
+        }
+
+        public override void Apply(TextEditBuffer buffer)
+        {
+            if (buffer.HasComposition)
+            {
+                buffer.Remove(buffer.Composition!.Value.Start, buffer.Composition!.Value.End - buffer.Composition!.Value.Start);
+                buffer.Insert(buffer.Composition!.Value.Start, _text);
+            }
+            else
+            {
+                buffer.Remove(buffer.Selection.Start, buffer.Selection.End - buffer.Selection.Start);
+                buffer.Insert(buffer.Selection.Start, _text);
+
+            }
+            var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition - _text.Length;
+            buffer.Selection = new TextSelection(newCursor, newCursor);
+        }
+    }
+
+    internal class FinishComposingCommand : EditCommand
+    {
+        public override void Apply(TextEditBuffer buffer)
+        {
+            buffer.Composition = default;
+        }
+    }
+}

+ 102 - 0
src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs

@@ -0,0 +1,102 @@
+using System;
+using Android.Views;
+using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.Android.Platform.Input
+{
+    internal class TextEditBuffer
+    {
+        private readonly IAndroidInputMethod _textInputMethod;
+        private readonly TopLevelImpl _topLevel;
+        private TextSelection? _composition;
+
+        public TextEditBuffer(IAndroidInputMethod textInputMethod, TopLevelImpl topLevel)
+        {
+            _textInputMethod = textInputMethod;
+            _topLevel = topLevel;
+        }
+
+        public bool HasComposition => Composition is { } composition && composition.Start != composition.End;
+
+        public TextSelection Selection
+        {
+            get => _textInputMethod.Client?.Selection ?? default; set
+            {
+                if (_textInputMethod.Client is { } client)
+                    client.Selection = value;
+            }
+        }
+
+        public TextSelection? Composition
+        {
+            get => _composition; set
+            {
+                if (value is { } v)
+                {
+                    var text = Text;
+                    var start = Math.Clamp(v.Start, 0, text.Length);
+                    var end = Math.Clamp(v.End, 0, text.Length);
+                    _composition = new TextSelection(start, end);
+                }
+                else
+                    _composition = null;
+            }
+        }
+
+        public string? SelectedText
+        {
+            get
+            {
+                if(_textInputMethod.Client is not { } client || Selection.Start < 0 || Selection.End >= client.SurroundingText.Length)
+                {
+                    return "";
+                }
+
+                return client.SurroundingText.Substring(Selection.Start, Selection.End - Selection.Start);
+            }
+        }
+
+        public string? ComposingText
+        {
+            get => !HasComposition ? null : Text?.Substring(Composition!.Value.Start, Composition!.Value.End - Composition!.Value.Start); set
+            {
+                if (HasComposition)
+                {
+                    var start = Composition!.Value.Start;
+                    Remove(start, Composition!.Value.End - start);
+                    Insert(start, value ?? "");
+                    Composition = new TextSelection(start, start  + (value?.Length ?? 0));
+                }
+                else
+                {
+                    var start = Selection.Start;
+                    Remove(start, Selection.End - start);
+                    Insert(start, value ?? "");
+                    Composition = new TextSelection(start, start + (value?.Length ?? 0));
+                }
+            }
+        }
+
+        public string Text => _textInputMethod.Client?.SurroundingText ?? "";
+
+        internal void Insert(int index, string text)
+        {
+            if (_textInputMethod.Client is { } client)
+            {
+                client.Selection = new TextSelection(index, index);
+                _topLevel.TextInput(text);
+            }
+        }
+
+        internal void Remove(int index, int length)
+        {
+            if (_textInputMethod.Client is { } client)
+            {
+                client.Selection = new TextSelection(index, index + length);
+                if (length > 0)
+                    _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
+            }
+        }
+    }
+}

+ 0 - 270
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using Android.App;
 using Android.Content;
 using Android.Graphics;
@@ -438,273 +437,4 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
     }
-
-    internal class EditableWrapper : SpannableStringBuilder
-    {
-        private readonly AvaloniaInputConnection _inputConnection;
-
-        public EditableWrapper(AvaloniaInputConnection inputConnection)
-        {
-            _inputConnection = inputConnection;
-        }
-
-        public TextSelection CurrentSelection => new TextSelection(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this));
-        public TextSelection CurrentComposition => new TextSelection(BaseInputConnection.GetComposingSpanStart(this), BaseInputConnection.GetComposingSpanEnd(this));
-
-        public bool IgnoreChange { get; set; }
-
-        public override IEditable? Replace(int start, int end, ICharSequence? tb)
-        {
-            if (!IgnoreChange && start != end)
-            {
-                SelectSurroundingTextForDeletion(start, end);
-            }
-
-            return base.Replace(start, end, tb);
-        }
-
-        public override IEditable? Replace(int start, int end, ICharSequence? tb, int tbstart, int tbend)
-        {
-            if (!IgnoreChange && start != end)
-            {
-                SelectSurroundingTextForDeletion(start, end);
-            }
-
-            return base.Replace(start, end, tb, tbstart, tbend);
-        }
-
-        private void SelectSurroundingTextForDeletion(int start, int end)
-        {
-            _inputConnection.InputMethod.Client!.Selection = new TextSelection(start, end);
-        }
-    }
-
-    internal class AvaloniaInputConnection : BaseInputConnection
-    {
-        private readonly TopLevelImpl _toplevel;
-        private readonly IAndroidInputMethod _inputMethod;
-        private readonly EditableWrapper _editable;
-        private bool _commitInProgress;
-        private int _batchLevel = 0;
-
-        public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
-        {
-            _toplevel = toplevel;
-            _inputMethod = inputMethod;
-            _editable = new EditableWrapper(this);
-        }
-
-        public int ExtractedTextToken { get; private set; }
-
-        public override IEditable Editable => _editable;
-
-        public EditableWrapper EditableWrapper => _editable;
-
-        public IAndroidInputMethod InputMethod => _inputMethod;
-
-        public TopLevelImpl Toplevel => _toplevel;
-
-        public bool IsInBatchEdit => _batchLevel > 0;
-
-        public override bool SetComposingRegion(int start, int end)
-        {
-            return base.SetComposingRegion(start, end);
-        }
-
-        public override bool SetComposingText(ICharSequence? text, int newCursorPosition)
-        {
-            if (InputMethod.Client is null || text is null)
-            {
-                return false;
-            }
-
-            BeginBatchEdit();
-            _editable.IgnoreChange = true;
-
-            try
-            {
-                if (_editable.CurrentComposition.Start > -1)
-                {
-                    // Select the composing region.
-                    InputMethod.Client.Selection = new TextSelection(_editable.CurrentComposition.Start, _editable.CurrentComposition.End);
-                }
-                var compositionText = text.SubSequence(0, text.Length());
-
-                if (_inputMethod.IsActive && !_commitInProgress)
-                {
-                    if (string.IsNullOrEmpty(compositionText))
-                    {
-                        if (_editable.CurrentComposition.Start > -1)
-                            _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
-                    }
-                    else
-                        _toplevel.TextInput(compositionText);
-                }
-                base.SetComposingText(text, newCursorPosition);
-            }
-            finally
-            {
-                _editable.IgnoreChange = false;
-
-                EndBatchEdit();
-            }
-
-            return true;
-        }
-
-        public override bool BeginBatchEdit()
-        {
-            _batchLevel = Interlocked.Increment(ref _batchLevel);
-            return base.BeginBatchEdit();
-        }
-
-        public override bool EndBatchEdit()
-        {
-            _batchLevel = Interlocked.Decrement(ref _batchLevel);
-
-            _inputMethod.OnBatchEditedEnded();
-            return base.EndBatchEdit();
-        }
-
-        public override bool CommitText(ICharSequence? text, int newCursorPosition)
-        {
-            if (InputMethod.Client is null || text is null)
-            {
-                return false;
-            }
-
-            BeginBatchEdit();
-            _commitInProgress = true;
-
-            var composingRegion = _editable.CurrentComposition;
-
-            var ret = base.CommitText(text, newCursorPosition);
-
-            if(composingRegion.Start != -1)
-            {
-                InputMethod.Client.Selection = composingRegion;
-            }
-
-            var committedText = text.SubSequence(0, text.Length());
-
-            if (_inputMethod.IsActive)
-                if (string.IsNullOrEmpty(committedText))
-                    _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
-                else
-                    _toplevel.TextInput(committedText);
-
-            _commitInProgress = false;
-            EndBatchEdit();
-
-            return true;
-        }
-
-        public override bool DeleteSurroundingText(int beforeLength, int afterLength)
-        {
-            if (InputMethod.IsActive)
-            {
-                EditableWrapper.IgnoreChange = true;
-            }
-
-            if (InputMethod.IsActive)
-            {
-                var selection = _editable.CurrentSelection;
-
-                InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength);
-
-                InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
-
-                EditableWrapper.IgnoreChange = true;
-            }
-
-            return true;
-        }
-
-        public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
-        {
-            switch (actionCode)
-            {
-                case ImeAction.Done:
-                    {
-                        _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly);
-                        break;
-                    }
-                case ImeAction.Next:
-                    {
-                        FocusManager.GetFocusManager(_toplevel.InputRoot)?
-                            .TryMoveFocus(NavigationDirection.Next);
-                        break;
-                    }
-            }
-
-            return base.PerformEditorAction(actionCode);
-        }
-
-        public override ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags)
-        {
-            if (request == null)
-                return null;
-
-            ExtractedTextToken = request.Token;
-
-            var editable = Editable;
-
-            if (editable == null)
-            {
-                return null;
-            }
-
-            if (!_inputMethod.IsActive)
-            {
-                return null;
-            }
-
-            var selection = _editable.CurrentSelection;
-
-            ExtractedText extract = new ExtractedText
-            {
-                Flags = 0,
-                PartialStartOffset = -1,
-                PartialEndOffset = -1,
-                SelectionStart = selection.Start,
-                SelectionEnd = selection.End,
-                StartOffset = 0
-            };
-
-            if ((request.Flags & GetTextFlags.WithStyles) != 0)
-            {
-                extract.Text = new SpannableString(editable);
-            }
-            else
-            {
-                extract.Text = editable;
-            }
-
-            return extract;
-        }
-
-        public override bool PerformContextMenuAction(int id)
-        {
-            if (InputMethod.Client is not { } client) return false;
-
-            switch (id)
-            {
-                case global::Android.Resource.Id.SelectAll:
-                    client.ExecuteContextMenuAction(ContextMenuAction.SelectAll);
-                    return true;
-                case global::Android.Resource.Id.Cut:
-                    client.ExecuteContextMenuAction(ContextMenuAction.Cut);
-                    return true;
-                case global::Android.Resource.Id.Copy:
-                    client.ExecuteContextMenuAction(ContextMenuAction.Copy);
-                    return true;
-                case global::Android.Resource.Id.Paste:
-                    client.ExecuteContextMenuAction(ContextMenuAction.Paste);
-                    return true;
-                default:
-                    break;
-            }
-            return base.PerformContextMenuAction(id);
-        }
-    }
 }