2#pragma warning disable CA1416
5using Android.Content.PM;
10using Android.Provider;
13using AndroidX.Core.View;
15using Silk.NET.Windowing.Sdl.Android;
17using Insets = AndroidX.Core.Graphics.Insets;
18using Path = System.IO.Path;
19using Stream = Android.Media.Stream;
20using Uri = Android.Net.Uri;
24 public class EngineActivity : SilkActivity {
25 internal static EngineActivity m_activity;
27 public event Action
Paused;
29 public event Action Resumed;
31 public event Action Destroyed;
33 public event Action<Intent> NewIntent;
35 public event Func<KeyEvent, bool> OnDispatchKeyEvent;
37 public static string BasePath = RunPath.AndroidFilePath;
38 public static string ConfigPath = RunPath.AndroidFilePath;
40 const int PickFileRequestCode = 1001;
41 TaskCompletionSource<(System.IO.Stream Stream,
string FileName)> filePickTcs;
43 AudioManager AudioManager {
46 field = GetAudioManager();
52 public EngineActivity() => m_activity =
this;
54 protected override void OnCreate(Bundle savedInstanceState) {
55 RequestWindowFeature(WindowFeatures.NoTitle);
56 base.OnCreate(savedInstanceState);
57 Window?.AddFlags(WindowManagerFlags.Fullscreen | WindowManagerFlags.TranslucentStatus | WindowManagerFlags.TranslucentNavigation);
58 EnableImmersiveMode();
59 VolumeControlStream = Stream.Music;
60 RequestedOrientation = ScreenOrientation.SensorLandscape;
61 if (Build.VERSION.SdkInt >= (BuildVersionCodes)28 && Window !=
null) {
62 ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView,
new ApplyWindowInsetsListener());
66 public void Vibrate(
long ms) {
67 if (Build.VERSION.SdkInt >= (BuildVersionCodes)26) {
68 (GetSystemService(
"vibrator") as Vibrator)?.Vibrate(VibrationEffect.CreateOneShot(ms, VibrationEffect.DefaultAmplitude));
72 public
void OpenLink(
string link) {
73 StartActivity(
new Intent(Intent.ActionView,
Uri.Parse(link)));
76 public void OpenFile(
string path,
string chooserTitle =
null,
string mimeType =
null) {
77 string processedAndroidFilePath = Storage.ProcessPath(RunPath.AndroidFilePath,
false,
false);
78 if (!path.StartsWith(processedAndroidFilePath)) {
79 throw new ArgumentException($
"Open {path} failed, because it is not in {processedAndroidFilePath}.");
81 Java.IO.File file =
new(path);
83 throw new FileNotFoundException($
"Open {path} failed, because it is not exists.");
85 Uri uri = Build.VERSION.SdkInt >= BuildVersionCodes.N
86 ? AndroidX.Core.Content.FileProvider.GetUriForFile(
this, $
"{PackageName}.fileprovider", file)
88 Intent intent =
new(Intent.ActionView);
89 mimeType ??= Android.Webkit.MimeTypeMap.Singleton?.GetMimeTypeFromExtension(Storage.GetExtension(path));
90 if (mimeType ==
null) {
94 intent.SetDataAndType(uri, mimeType);
96 intent.AddFlags(ActivityFlags.GrantReadUriPermission | ActivityFlags.NewTask);
97 if (Application.Context.PackageManager?.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly).Any() ??
false) {
98 StartActivity(Intent.CreateChooser(intent, chooserTitle ?? Storage.GetFileName(path)));
101 throw new InvalidOperationException($
"Open {path} failed, because no app can open it.");
105 public void ShareFile(
string path,
string chooserTitle =
null,
string mimeType =
null) {
106 string processedAndroidFilePath = Storage.ProcessPath(RunPath.AndroidFilePath,
false,
false);
107 if (!path.StartsWith(processedAndroidFilePath)) {
108 throw new ArgumentException($
"Share {path} failed, because it is not in {processedAndroidFilePath}.");
110 Java.IO.File file =
new(path);
111 if (!file.Exists()) {
112 throw new FileNotFoundException($
"Share {path} failed, because it does not exist.");
114 Uri uri = Build.VERSION.SdkInt >= BuildVersionCodes.N
115 ? AndroidX.Core.Content.FileProvider.GetUriForFile(
this, $
"{PackageName}.fileprovider", file)
116 :
Uri.FromFile(file);
117 Intent intent =
new(Intent.ActionSend);
118 mimeType ??= Android.Webkit.MimeTypeMap.Singleton?.GetMimeTypeFromExtension(Storage.GetExtension(path)) ??
"*/*";
119 intent.SetType(mimeType);
120 intent.PutExtra(Intent.ExtraStream, uri);
121 intent.AddFlags(ActivityFlags.GrantReadUriPermission | ActivityFlags.NewTask);
122 StartActivity(Intent.CreateChooser(intent, chooserTitle ?? Storage.GetFileName(path)));
125 public Task<(System.IO.Stream Stream,
string FileName)> ChooseFileAsync(
string chooserTitle =
null) {
126 Intent intent =
new(Intent.ActionOpenDocument);
127 intent.AddCategory(Intent.CategoryOpenable);
128 intent.SetType(
"*/*");
129 filePickTcs =
new TaskCompletionSource<(System.IO.Stream, string)>();
130 StartActivityForResult(
string.IsNullOrEmpty(chooserTitle) ? intent : Intent.CreateChooser(intent, chooserTitle), PickFileRequestCode);
131 return filePickTcs.Task;
134 protected override void OnActivityResult(
int requestCode, [GeneratedEnum] Result resultCode, Intent data) {
135 base.OnActivityResult(requestCode, resultCode, data);
136 if (requestCode == PickFileRequestCode) {
137 if (resultCode == Result.Ok
140 System.IO.Stream stream = GetStreamFromUri(data.Data, out
string fileName);
141 filePickTcs?.TrySetResult((stream, fileName));
143 catch (Exception ex) {
144 filePickTcs?.TrySetException(ex);
148 filePickTcs?.TrySetResult((
null,
null));
153 protected override void OnPause() {
158 protected override void OnResume() {
163 protected override void OnNewIntent(Intent intent) {
164 base.OnNewIntent(intent);
165 NewIntent?.Invoke(intent);
168 protected override void OnRun() { }
170 protected override void OnDestroy() {
181 public override bool DispatchTouchEvent(MotionEvent e) {
185 if ((e.Source & InputSourceType.Touchscreen) == InputSourceType.Touchscreen) {
186 Touch.HandleTouchEvent(e);
188 else if ((e.Source & InputSourceType.Mouse) == InputSourceType.Mouse
189 || (e.Source & InputSourceType.ClassPointer) == InputSourceType.ClassPointer
190 || (e.Source & InputSourceType.MouseRelative) == InputSourceType.MouseRelative) {
191 Mouse.HandleMotionEvent(e);
196 public override bool DispatchKeyEvent(KeyEvent e) {
203 bool handled =
false;
204 Delegate[] invocationList = OnDispatchKeyEvent?.GetInvocationList();
205 if (invocationList !=
null) {
206 foreach (Delegate invocation
in invocationList) {
207 handled |= (bool)invocation.DynamicInvoke(e)!;
211 _ = e.Action
switch {
212 KeyEventActions.Down => OnKeyDown(e.KeyCode, e),
213 KeyEventActions.Up => OnKeyUp(e.KeyCode, e),
220 public override bool OnKeyDown(Keycode keyCode, KeyEvent e) {
222 case Keycode.VolumeUp:
223 AudioManager?.AdjustStreamVolume(Stream.Music, Adjust.Raise, VolumeNotificationFlags.ShowUi);
224 EnableImmersiveMode();
226 case Keycode.VolumeDown:
227 AudioManager?.AdjustStreamVolume(Stream.Music, Adjust.Lower, VolumeNotificationFlags.ShowUi);
228 EnableImmersiveMode();
234 if ((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad
235 || (e.Source & InputSourceType.Joystick) == InputSourceType.Joystick) {
236 GamePad.HandleKeyEvent(e);
244 AudioManager GetAudioManager() => Build.VERSION.SdkInt >= (BuildVersionCodes)21 ? GetSystemService(
"audio") as AudioManager :
null;
246 public override bool OnKeyUp(Keycode keyCode, KeyEvent e) {
250 if ((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad
251 || (e.Source & InputSourceType.Joystick) == InputSourceType.Joystick) {
252 GamePad.HandleKeyEvent(e);
260 public override bool DispatchGenericMotionEvent(MotionEvent e) {
265 if (((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad || (e.Source & InputSourceType.Joystick) == InputSourceType.Joystick)
266 && e.Action == MotionEventActions.Move) {
267 GamePad.HandleMotionEvent(e);
269 if ((e.Source & InputSourceType.Mouse) == InputSourceType.Mouse
270 || (e.Source & InputSourceType.ClassPointer) == InputSourceType.ClassPointer
271 || (e.Source & InputSourceType.MouseRelative) == InputSourceType.MouseRelative) {
272 Mouse.HandleMotionEvent(e);
277 public void EnableImmersiveMode() {
278 if (Window !=
null) {
279 switch (Build.VERSION.SdkInt) {
280 case >= (BuildVersionCodes)30:
281 IWindowInsetsController insetsController = Window.InsetsController;
282 if (insetsController !=
null) {
283 insetsController.Hide(WindowInsets.Type.SystemBars());
284 insetsController.SystemBarsBehavior = (int)WindowInsetsControllerBehavior.ShowTransientBarsBySwipe;
287#pragma warning disable CA1422
288 case > (BuildVersionCodes)19:
289 Window.DecorView.SystemUiFlags = SystemUiFlags.
Fullscreen
290 | SystemUiFlags.HideNavigation
291 | SystemUiFlags.Immersive
292 | SystemUiFlags.ImmersiveSticky;
break;
293#pragma warning restore CA1422
298 public void GetGlEsVersion(out
int major, out
int minor) {
300 int reqGlEsVersion = ((ActivityManager)GetSystemService(ActivityService))?.DeviceConfigurationInfo?.ReqGlEsVersion ?? 0x20000;
301 major = reqGlEsVersion >> 16;
302 minor = reqGlEsVersion & 0xFFFF;
310 public System.IO.Stream GetStreamFromUri(
Uri uri, out
string fileName) {
311 System.IO.Stream stream =
null;
314 using (ICursor cursor = ContentResolver?.Query(uri,
null,
null,
null,
null)) {
316 && cursor.MoveToFirst()) {
317 int nameIndex = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
318 if (nameIndex >= 0) {
319 fileName = cursor.GetString(nameIndex);
323 stream = ContentResolver?.OpenInputStream(uri);
328 if (
string.IsNullOrEmpty(fileName)) {
329 fileName = Path.GetFileName(uri.Path);
334 public class ApplyWindowInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener {
335 public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) {
336 IList<Rect> boundingRects = insets?.DisplayCutout?.BoundingRects;
337 if (boundingRects ==
null
338 || boundingRects.Count == 0) {
339 return WindowInsetsCompat.Consumed;
341 bool hasWideNotch =
false;
342 if (boundingRects.Count >= 2) {
346 Rect rect = boundingRects[0];
347 if (Math.Max(rect.Width(), rect.Height()) > 200) {
351 Insets cutoutInsets = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
352 if (cutoutInsets !=
null) {
353 Engine.Window.DisplayCutoutInsetsChangedHandler(
354 new Vector4(cutoutInsets.Left, cutoutInsets.Top, cutoutInsets.Right, cutoutInsets.Bottom),
358 return WindowInsetsCompat.Consumed;
System.Environment Environment