using global::Android.Views; namespace DodoSSH.Client.Android.Platform; /// /// Takes the keyboard away from the terminal's WebView, and reports the taps that should bring it /// back. /// /// /// /// This replaces TerminalFocus, and it is the opposite move. That class handed Android's own /// view focus back to the WebView after Avalonia's DispatchTouchEvent took it, because /// the input connection the keyboard was attached to belonged to the page. Under ADR 0015 it no longer does: /// is the input target, and it is an Avalonia control, so the focus /// the platform requests for Avalonia's view after every handled touch is now the focus we want. The fight /// ends by removing one of the contestants rather than by refereeing it faster. /// /// /// Non-focusable, not non-interactive, and the distinction is the whole design. Focusability governs /// where keyboard and D-pad input go; it has nothing to do with touch delivery. A WebView with /// Focusable=false still receives, scrolls and hit-tests every touch it is given — so the page keeps /// its touch-action: pan-y scrollback exactly as before — but it can no longer take an input /// connection, and Chromium's own showSoftInput for the focused DOM node cannot succeed from an /// unfocused view. That is precisely the split wanted: the page keeps the finger, Avalonia keeps the /// keyboard. /// /// /// Why a tap has to be reported at all. With the page unable to raise a keyboard, the ordinary way /// back after the system back gesture lowers one — touch the terminal — would otherwise do nothing. The /// listener is attached with Handled = false throughout, so it observes without consuming: the /// WebView's own OnTouchEvent runs after it exactly as if nothing were listening. /// /// /// A tap is distinguished from a scroll the way every Android view does it, by /// ViewConfiguration.ScaledTouchSlop. Without that test, dragging the scrollback would raise the /// keyboard over the very output being scrolled to. /// /// /// Found by walking the decor view rather than asked of the NativeWebView control, for the reason /// TerminalFocus gave and which has not changed: the control does not expose its platform child, and /// this application only ever has the one WebView, so the walk's first match is necessarily the /// terminal. Every step is allowed to be absent — there is no activity while backgrounded, and no /// WebView until the terminal surface has been shown once. /// /// internal static class RendererFocus { /// The view already configured, so that showing the terminal twice does not listen twice. /// /// Weak, because this is a static holding an Android view: a strong reference would keep the whole view /// hierarchy, and the activity behind it, alive for the life of the process. /// private static WeakReference? configured; /// /// Makes the renderer unable to hold the keyboard, and calls when somebody /// taps it. /// /// /// Idempotent, and called on every transition into the terminal rather than once at startup, because the /// WebView does not exist until the surface it lives on has been shown — and because Android is /// free to tear its renderer down and build a new one, which is a case this head already handles /// elsewhere (see docs/android-port.md). /// public static void Suppress(Action onTapped) { if (PhoneEnvironment.CurrentActivity?.Window?.DecorView is not ViewGroup decor) { return; } if (FindWebView(decor) is not { } webView) { return; } if (configured is not null && configured.TryGetTarget(out var already) && ReferenceEquals(already, webView)) { return; } webView.Focusable = false; webView.FocusableInTouchMode = false; webView.ClearFocus(); WatchForTaps(webView, onTapped); configured = new WeakReference(webView); } /// /// Calls for a tap, and stays out of the way of everything else. /// /// /// A tap is a press and a release that did not travel further than the platform's own /// ScaledTouchSlop — the same test every Android view uses, and necessary rather than fastidious: /// without it, dragging the scrollback would raise the keyboard over the output being scrolled to. /// private static void WatchForTaps(global::Android.Webkit.WebView webView, Action onTapped) { // The fallback is the platform's own historical default, for a context that has somehow gone. var slop = webView.Context is { } context ? ViewConfiguration.Get(context)?.ScaledTouchSlop ?? 8 : 8; var downX = 0f; var downY = 0f; var moved = false; webView.Touch += (_, e) => { // Observed, never consumed. The WebView's own OnTouchEvent still runs, so the page scrolls. e.Handled = false; if (e.Event is not { } motion) { return; } switch (motion.Action) { case MotionEventActions.Down: downX = motion.GetX(); downY = motion.GetY(); moved = false; break; case MotionEventActions.Move: moved |= Math.Abs(motion.GetX() - downX) > slop || Math.Abs(motion.GetY() - downY) > slop; break; case MotionEventActions.Up when !moved: onTapped(); break; } }; } private static global::Android.Webkit.WebView? FindWebView(ViewGroup parent) { for (var i = 0; i < parent.ChildCount; i++) { switch (parent.GetChildAt(i)) { case global::Android.Webkit.WebView webView: return webView; case ViewGroup child when FindWebView(child) is { } found: return found; } } return null; } }