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;
}
}