using System; using System.Collections.Generic; using System.Runtime.Versioning; using Android.App; using Android.Content; using Android.Graphics; using Android.Graphics.Drawables; using Android.OS; using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; using AndroidX.AppCompat.App; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; using ClipboardManager = Android.Content.ClipboardManager; namespace Avalonia.Android.Platform.SkiaPlatform { class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidInputMethod _textInputMethod; private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; private readonly AndroidInsetsManager _insetsManager; private readonly ClipboardImpl _clipboard; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) { _view = new ViewImpl(avaloniaView.Context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this); _gl = new EglGlPlatformSurface(this); _framebuffer = new FramebufferManager(this); _clipboard = new ClipboardImpl(avaloniaView.Context?.GetSystemService(Context.ClipboardService).JavaCast()); RenderScaling = _view.Scaling; MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); if (avaloniaView.Context is AvaloniaMainActivity mainActivity) { _insetsManager = new AndroidInsetsManager(mainActivity, this); } _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling; public IInputRoot InputRoot { get; private set; } public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } public Action Input { get; set; } public Size MaxClientSize { get; protected set; } public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } public View View => _view; internal InvalidationAwareSurfaceView InternalView => _view; public IPlatformHandle Handle => _view; public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces); public virtual void Hide() { _view.Visibility = ViewStates.Invisible; } public void Invalidate(Rect rect) { if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate(); } public Point PointToClient(PixelPoint point) { return point.ToPoint(RenderScaling); } public PixelPoint PointToScreen(Point point) { return PixelPoint.FromPoint(point, RenderScaling); } public void SetCursor(ICursorImpl cursor) { //still not implemented } public void SetInputRoot(IInputRoot inputRoot) { InputRoot = inputRoot; } public virtual void Show() { _view.Visibility = ViewStates.Visible; } public double RenderScaling { get; } void Draw() { Paint?.Invoke(new Rect(new Point(0, 0), ClientSize)); } public virtual void Dispose() { _view.Dispose(); _view = null; } protected virtual void OnResized(Size size) { Resized?.Invoke(size, WindowResizeReason.Unspecified); } internal void Resize(Size size) { Resized?.Invoke(size, WindowResizeReason.Layout); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo { private readonly TopLevelImpl _tl; private Size _oldSize; public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context) { _tl = tl; if (placeOnTop) SetZOrderOnTop(true); } public TopLevelImpl TopLevelImpl => _tl; protected override void Draw() { _tl.Draw(); } protected override void DispatchDraw(global::Android.Graphics.Canvas canvas) { // Workaround issue #9230 on where screen remains gray after splash screen. // base.DispatchDraw should punch a hole into the canvas so the surface // can be seen below, but it does not. if (OperatingSystem.IsAndroidVersionAtLeast(29)) { // Android 10+ does this (BlendMode was new) var paint = new Paint(); paint.SetColor(0); paint.BlendMode = BlendMode.Clear; canvas.DrawRect(0, 0, Width, Height, paint); } else { // Android 9 did this canvas.DrawColor(Color.Transparent, PorterDuff.Mode.Clear); } base.DispatchDraw(canvas); } protected override bool DispatchGenericPointerEvent(MotionEvent e) { bool callBase; bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); bool baseResult = callBase ? base.DispatchGenericPointerEvent(e) : false; return result != null ? result.Value : baseResult; } public override bool DispatchTouchEvent(MotionEvent e) { bool callBase; bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); bool baseResult = callBase ? base.DispatchTouchEvent(e) : false; return result != null ? result.Value : baseResult; } public override bool DispatchKeyEvent(KeyEvent e) { bool callBase; bool? res = _tl._keyboardHelper.DispatchKeyEvent(e, out callBase); bool baseResult = callBase ? base.DispatchKeyEvent(e) : false; return res != null ? res.Value : baseResult; } void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling); if (newSize != _oldSize) { _oldSize = newSize; _tl.OnResized(newSize); } base.SurfaceChanged(holder, format, width, height); } public sealed override bool OnCheckIsTextEditor() { return true; } private Func _initEditorInfo; public void InitEditorInfo(Func init) { _initEditorInfo = init; } public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) { if (_initEditorInfo != null) { return _initEditorInfo(_tl, outAttrs); } return null; } } public IPopupImpl CreatePopup() => null; public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } public WindowTransparencyLevel TransparencyLevel { get; private set; } public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { if(_insetsManager != null) { _insetsManager.SystemBarTheme = themeVariant switch { PlatformThemeVariant.Light => SystemBarTheme.Light, PlatformThemeVariant.Dark => SystemBarTheme.Dark, _ => null, }; } AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; public PixelSize Size => _view.Size; public double Scaling => RenderScaling; public void SetTransparencyLevelHint(IReadOnlyList transparencyLevels) { if (_view.Context is not AvaloniaMainActivity activity) return; foreach (var level in transparencyLevels) { if (!IsSupported(level)) { continue; } if (level == WindowTransparencyLevel.None && OperatingSystem.IsAndroidVersionAtLeast(30)) { activity.SetTranslucent(false); } else if (level == WindowTransparencyLevel.Transparent) { SetTransparencyTransparent(activity); } else if (level == WindowTransparencyLevel.Blur) { SetTransparencyBlur(activity); } TransparencyLevel = level; break; } } public virtual object TryGetFeature(Type featureType) { if (featureType == typeof(IStorageProvider)) { return _storageProvider; } if (featureType == typeof(ITextInputMethodImpl)) { return _textInputMethod; } if (featureType == typeof(ISystemNavigationManagerImpl)) { return _systemNavigationManager; } if (featureType == typeof(INativeControlHostImpl)) { return _nativeControlHost; } if (featureType == typeof(IInsetsManager)) { return _insetsManager; } if(featureType == typeof(IClipboard)) { return _clipboard; } return null; } private static bool IsSupported(WindowTransparencyLevel level) { if (level == WindowTransparencyLevel.None) return true; if (level == WindowTransparencyLevel.Transparent) return OperatingSystem.IsAndroidVersionAtLeast(30); if (level == WindowTransparencyLevel.Blur) return OperatingSystem.IsAndroidVersionAtLeast(31); return false; } private static void SetTransparencyTransparent(AvaloniaMainActivity activity) { if (OperatingSystem.IsAndroidVersionAtLeast(30)) { activity.SetTranslucent(true); SetBlurBehindRadius(activity, 0); activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent)); } } private static void SetTransparencyBlur(AvaloniaMainActivity activity) { if (OperatingSystem.IsAndroidVersionAtLeast(31)) { activity.SetTranslucent(true); activity.Window?.AddFlags(WindowManagerFlags.BlurBehind); SetBlurBehindRadius(activity, 120); activity.Window?.SetBackgroundDrawable(new ColorDrawable(Color.Transparent)); } } private static void SetBlurBehindRadius(AvaloniaMainActivity activity, int radius) { if (OperatingSystem.IsAndroidVersionAtLeast(31) && activity.Window?.Attributes is { } attr) { attr.BlurBehindRadius = radius; activity.Window.Attributes = attr; } } } internal class AvaloniaInputConnection : BaseInputConnection { private readonly TopLevelImpl _topLevel; private readonly IAndroidInputMethod _inputMethod; private readonly InputEditable _editable; public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { _topLevel = topLevel; _inputMethod = inputMethod; _editable = new InputEditable(_topLevel, _inputMethod, this); } public override IEditable Editable => _editable; internal InputEditable InputEditable => _editable; public override bool SetComposingRegion(int start, int end) { var ret = base.SetComposingRegion(start, end); InputEditable.RaiseCompositionChanged(); return ret; } public override bool SetComposingText(ICharSequence text, int newCursorPosition) { var composingText = text.ToString(); if (string.IsNullOrEmpty(composingText)) { return CommitText(text, newCursorPosition); } else { var ret = base.SetComposingText(text, newCursorPosition); InputEditable.RaiseCompositionChanged(); return ret; } } public override bool BeginBatchEdit() { _editable.BeginBatchEdit(); return base.BeginBatchEdit(); } public override bool EndBatchEdit() { var ret = base.EndBatchEdit(); _editable.EndBatchEdit(); return ret; } public override bool FinishComposingText() { var ret = base.FinishComposingText(); InputEditable.RaiseCompositionChanged(); return ret; } public override bool CommitText(ICharSequence text, int newCursorPosition) { var ret = base.CommitText(text, newCursorPosition); InputEditable.RaiseCompositionChanged(); return ret; } public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) { case ImeAction.Done: { _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly); break; } } return base.PerformEditorAction(actionCode); } } }