Browse Source

Merge pull request #76821 from m4gr3d/prototype_godot_service_main

Refactor Godot Android architecture
Yuri Sizov 2 years ago
parent
commit
57919beb05
20 changed files with 1701 additions and 1477 deletions
  1. 2 2
      platform/android/java/app/src/com/godot/game/GodotApp.java
  2. 4 4
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
  3. 4 150
      platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
  4. 0 1195
      platform/android/java/lib/src/org/godotengine/godot/Godot.java
  5. 965 0
      platform/android/java/lib/src/org/godotengine/godot/Godot.kt
  6. 167 0
      platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
  7. 429 0
      platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
  8. 12 8
      platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
  9. 8 1
      platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
  10. 5 0
      platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
  11. 54 0
      platform/android/java/lib/src/org/godotengine/godot/GodotService.kt
  12. 9 4
      platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
  13. 5 4
      platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
  14. 0 2
      platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java
  15. 6 5
      platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java
  16. 16 16
      platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
  17. 11 1
      platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
  18. 1 1
      platform/android/java_godot_lib_jni.cpp
  19. 3 73
      platform/android/java_godot_wrapper.cpp
  20. 0 11
      platform/android/java_godot_wrapper.h

+ 2 - 2
platform/android/java/app/src/com/godot/game/GodotApp.java

@@ -30,7 +30,7 @@
 
 package com.godot.game;
 
-import org.godotengine.godot.FullScreenGodotApp;
+import org.godotengine.godot.GodotActivity;
 
 import android.os.Bundle;
 
@@ -38,7 +38,7 @@ import android.os.Bundle;
  * Template activity for Godot Android builds.
  * Feel free to extend and modify this class for your custom logic.
  */
-public class GodotApp extends FullScreenGodotApp {
+public class GodotApp extends GodotActivity {
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		setTheme(R.style.GodotAppMainTheme);

+ 4 - 4
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt

@@ -39,7 +39,7 @@ import android.os.*
 import android.util.Log
 import android.widget.Toast
 import androidx.window.layout.WindowMetricsCalculator
-import org.godotengine.godot.FullScreenGodotApp
+import org.godotengine.godot.GodotActivity
 import org.godotengine.godot.GodotLib
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.ProcessPhoenix
@@ -55,7 +55,7 @@ import kotlin.math.min
  *
  * It also plays the role of the primary editor window.
  */
-open class GodotEditor : FullScreenGodotApp() {
+open class GodotEditor : GodotActivity() {
 
 	companion object {
 		private val TAG = GodotEditor::class.java.simpleName
@@ -115,7 +115,7 @@ open class GodotEditor : FullScreenGodotApp() {
 
 		runOnUiThread {
 			// Enable long press, panning and scaling gestures
-			godotFragment?.renderView?.inputHandler?.apply {
+			godotFragment?.godot?.renderView?.inputHandler?.apply {
 				enableLongPress(longPressEnabled)
 				enablePanningAndScalingGestures(panScaleEnabled)
 			}
@@ -318,7 +318,7 @@ open class GodotEditor : FullScreenGodotApp() {
 
 	override fun onRequestPermissionsResult(
 		requestCode: Int,
-		permissions: Array<String?>,
+		permissions: Array<String>,
 		grantResults: IntArray
 	) {
 		super.onRequestPermissionsResult(requestCode, permissions, grantResults)

+ 4 - 150
platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java

@@ -30,156 +30,10 @@
 
 package org.godotengine.godot;
 
-import org.godotengine.godot.utils.ProcessPhoenix;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-
 /**
- * Base activity for Android apps intending to use Godot as the primary and only screen.
+ * Base abstract activity for Android apps intending to use Godot as the primary screen.
  *
- * It's also a reference implementation for how to setup and use the {@link Godot} fragment
- * within an Android app.
+ * @deprecated Use {@link GodotActivity}
  */
-public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost {
-	private static final String TAG = FullScreenGodotApp.class.getSimpleName();
-
-	protected static final String EXTRA_FORCE_QUIT = "force_quit_requested";
-	protected static final String EXTRA_NEW_LAUNCH = "new_launch_requested";
-
-	@Nullable
-	private Godot godotFragment;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		setContentView(R.layout.godot_app_layout);
-
-		handleStartIntent(getIntent(), true);
-
-		Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container);
-		if (currentFragment instanceof Godot) {
-			Log.v(TAG, "Reusing existing Godot fragment instance.");
-			godotFragment = (Godot)currentFragment;
-		} else {
-			Log.v(TAG, "Creating new Godot fragment instance.");
-			godotFragment = initGodotInstance();
-			getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss();
-		}
-	}
-
-	@Override
-	public void onDestroy() {
-		Log.v(TAG, "Destroying Godot app...");
-		super.onDestroy();
-		terminateGodotInstance(godotFragment);
-	}
-
-	@Override
-	public final void onGodotForceQuit(Godot instance) {
-		runOnUiThread(() -> {
-			terminateGodotInstance(instance);
-		});
-	}
-
-	private void terminateGodotInstance(Godot instance) {
-		if (instance == godotFragment) {
-			Log.v(TAG, "Force quitting Godot instance");
-			ProcessPhoenix.forceQuit(FullScreenGodotApp.this);
-		}
-	}
-
-	@Override
-	public final void onGodotRestartRequested(Godot instance) {
-		runOnUiThread(() -> {
-			if (instance == godotFragment) {
-				// It's very hard to properly de-initialize Godot on Android to restart the game
-				// from scratch. Therefore, we need to kill the whole app process and relaunch it.
-				//
-				// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
-				// releasing and reloading native libs or resetting their state somehow and clearing static data).
-				Log.v(TAG, "Restarting Godot instance...");
-				ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this);
-			}
-		});
-	}
-
-	@Override
-	public void onNewIntent(Intent intent) {
-		super.onNewIntent(intent);
-		setIntent(intent);
-
-		handleStartIntent(intent, false);
-
-		if (godotFragment != null) {
-			godotFragment.onNewIntent(intent);
-		}
-	}
-
-	private void handleStartIntent(Intent intent, boolean newLaunch) {
-		boolean forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false);
-		if (forceQuitRequested) {
-			Log.d(TAG, "Force quit requested, terminating..");
-			ProcessPhoenix.forceQuit(this);
-			return;
-		}
-
-		if (!newLaunch) {
-			boolean newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false);
-			if (newLaunchRequested) {
-				Log.d(TAG, "New launch requested, restarting..");
-
-				Intent restartIntent = new Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false);
-				ProcessPhoenix.triggerRebirth(this, restartIntent);
-				return;
-			}
-		}
-	}
-
-	@CallSuper
-	@Override
-	public void onActivityResult(int requestCode, int resultCode, Intent data) {
-		super.onActivityResult(requestCode, resultCode, data);
-		if (godotFragment != null) {
-			godotFragment.onActivityResult(requestCode, resultCode, data);
-		}
-	}
-
-	@CallSuper
-	@Override
-	public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
-		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-		if (godotFragment != null) {
-			godotFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
-		}
-	}
-
-	@Override
-	public void onBackPressed() {
-		if (godotFragment != null) {
-			godotFragment.onBackPressed();
-		} else {
-			super.onBackPressed();
-		}
-	}
-
-	/**
-	 * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}.
-	 */
-	@NonNull
-	protected Godot initGodotInstance() {
-		return new Godot();
-	}
-
-	@Nullable
-	protected final Godot getGodotFragment() {
-		return godotFragment;
-	}
-}
+@Deprecated
+public abstract class FullScreenGodotApp extends GodotActivity {}

+ 0 - 1195
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -1,1195 +0,0 @@
-/**************************************************************************/
-/*  Godot.java                                                            */
-/**************************************************************************/
-/*                         This file is part of:                          */
-/*                             GODOT ENGINE                               */
-/*                        https://godotengine.org                         */
-/**************************************************************************/
-/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
-/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
-/*                                                                        */
-/* Permission is hereby granted, free of charge, to any person obtaining  */
-/* a copy of this software and associated documentation files (the        */
-/* "Software"), to deal in the Software without restriction, including    */
-/* without limitation the rights to use, copy, modify, merge, publish,    */
-/* distribute, sublicense, and/or sell copies of the Software, and to     */
-/* permit persons to whom the Software is furnished to do so, subject to  */
-/* the following conditions:                                              */
-/*                                                                        */
-/* The above copyright notice and this permission notice shall be         */
-/* included in all copies or substantial portions of the Software.        */
-/*                                                                        */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
-/**************************************************************************/
-
-package org.godotengine.godot;
-
-import static android.content.Context.MODE_PRIVATE;
-import static android.content.Context.WINDOW_SERVICE;
-
-import org.godotengine.godot.input.GodotEditText;
-import org.godotengine.godot.io.directory.DirectoryAccessHandler;
-import org.godotengine.godot.io.file.FileAccessHandler;
-import org.godotengine.godot.plugin.GodotPlugin;
-import org.godotengine.godot.plugin.GodotPluginRegistry;
-import org.godotengine.godot.tts.GodotTTS;
-import org.godotengine.godot.utils.BenchmarkUtils;
-import org.godotengine.godot.utils.GodotNetUtils;
-import org.godotengine.godot.utils.PermissionsUtil;
-import org.godotengine.godot.xr.XRMode;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.AlertDialog;
-import android.app.PendingIntent;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.content.pm.ConfigurationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Messenger;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.Surface;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewTreeObserver;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.view.WindowInsetsAnimation;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
-import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
-import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
-import com.google.android.vending.expansion.downloader.Helpers;
-import com.google.android.vending.expansion.downloader.IDownloaderClient;
-import com.google.android.vending.expansion.downloader.IDownloaderService;
-import com.google.android.vending.expansion.downloader.IStub;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-
-public class Godot extends Fragment implements SensorEventListener, IDownloaderClient {
-	private static final String TAG = Godot.class.getSimpleName();
-
-	private IStub mDownloaderClientStub;
-	private TextView mStatusText;
-	private TextView mProgressFraction;
-	private TextView mProgressPercent;
-	private TextView mAverageSpeed;
-	private TextView mTimeRemaining;
-	private ProgressBar mPB;
-	private ClipboardManager mClipboard;
-
-	private View mDashboard;
-	private View mCellMessage;
-
-	private Button mPauseButton;
-	private Button mWiFiSettingsButton;
-
-	private XRMode xrMode = XRMode.REGULAR;
-	private boolean use_immersive = false;
-	private boolean use_debug_opengl = false;
-	private boolean mStatePaused;
-	private boolean activityResumed;
-	private int mState;
-
-	private GodotHost godotHost;
-	private GodotPluginRegistry pluginRegistry;
-
-	static private Intent mCurrentIntent;
-
-	public void onNewIntent(Intent intent) {
-		mCurrentIntent = intent;
-	}
-
-	static public Intent getCurrentIntent() {
-		return mCurrentIntent;
-	}
-
-	private void setState(int newState) {
-		if (mState != newState) {
-			mState = newState;
-			mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
-		}
-	}
-
-	private void setButtonPausedState(boolean paused) {
-		mStatePaused = paused;
-		int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
-		mPauseButton.setText(stringResourceID);
-	}
-
-	private String[] command_line;
-	private boolean use_apk_expansion;
-
-	private ViewGroup containerLayout;
-	public GodotRenderView mRenderView;
-	private boolean godot_initialized = false;
-
-	private SensorManager mSensorManager;
-	private Sensor mAccelerometer;
-	private Sensor mGravity;
-	private Sensor mMagnetometer;
-	private Sensor mGyroscope;
-
-	public GodotIO io;
-	public GodotNetUtils netUtils;
-	public GodotTTS tts;
-	private DirectoryAccessHandler directoryAccessHandler;
-	private FileAccessHandler fileAccessHandler;
-
-	public interface ResultCallback {
-		void callback(int requestCode, int resultCode, Intent data);
-	}
-	public ResultCallback result_callback;
-
-	@Override
-	public void onAttach(Context context) {
-		super.onAttach(context);
-		if (getParentFragment() instanceof GodotHost) {
-			godotHost = (GodotHost)getParentFragment();
-		} else if (getActivity() instanceof GodotHost) {
-			godotHost = (GodotHost)getActivity();
-		}
-	}
-
-	@Override
-	public void onDetach() {
-		super.onDetach();
-		godotHost = null;
-	}
-
-	@CallSuper
-	@Override
-	public void onActivityResult(int requestCode, int resultCode, Intent data) {
-		super.onActivityResult(requestCode, resultCode, data);
-		if (result_callback != null) {
-			result_callback.callback(requestCode, resultCode, data);
-			result_callback = null;
-		}
-
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onMainActivityResult(requestCode, resultCode, data);
-		}
-	}
-
-	@CallSuper
-	@Override
-	public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
-		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults);
-		}
-
-		for (int i = 0; i < permissions.length; i++) {
-			GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED);
-		}
-	}
-
-	/**
-	 * Invoked on the render thread when the Godot setup is complete.
-	 */
-	@CallSuper
-	protected void onGodotSetupCompleted() {
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onGodotSetupCompleted();
-		}
-
-		if (godotHost != null) {
-			godotHost.onGodotSetupCompleted();
-		}
-	}
-
-	/**
-	 * Invoked on the render thread when the Godot main loop has started.
-	 */
-	@CallSuper
-	protected void onGodotMainLoopStarted() {
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onGodotMainLoopStarted();
-		}
-
-		if (godotHost != null) {
-			godotHost.onGodotMainLoopStarted();
-		}
-	}
-
-	/**
-	 * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.
-	 */
-	@Keep
-	private boolean onVideoInit() {
-		final Activity activity = requireActivity();
-		containerLayout = new FrameLayout(activity);
-		containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-
-		// GodotEditText layout
-		GodotEditText editText = new GodotEditText(activity);
-		editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
-				(int)getResources().getDimension(R.dimen.text_edit_height)));
-		// ...add to FrameLayout
-		containerLayout.addView(editText);
-
-		tts = new GodotTTS(activity);
-
-		if (!GodotLib.setup(command_line, tts)) {
-			Log.e(TAG, "Unable to setup the Godot engine! Aborting...");
-			alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit);
-			return false;
-		}
-
-		if (usesVulkan()) {
-			if (!meetsVulkanRequirements(activity.getPackageManager())) {
-				alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit);
-				return false;
-			}
-			mRenderView = new GodotVulkanRenderView(activity, this);
-		} else {
-			// Fallback to openGl
-			mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl);
-		}
-
-		View view = mRenderView.getView();
-		containerLayout.addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-		editText.setView(mRenderView);
-		io.setEdit(editText);
-
-		// Listeners for keyboard height.
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-			// Report the height of virtual keyboard as it changes during the animation.
-			final View decorView = activity.getWindow().getDecorView();
-			decorView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) {
-				int startBottom, endBottom;
-				@Override
-				public void onPrepare(@NonNull WindowInsetsAnimation animation) {
-					startBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
-				}
-
-				@NonNull
-				@Override
-				public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) {
-					endBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
-					return bounds;
-				}
-
-				@NonNull
-				@Override
-				public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) {
-					// Find the IME animation.
-					WindowInsetsAnimation imeAnimation = null;
-					for (WindowInsetsAnimation animation : list) {
-						if ((animation.getTypeMask() & WindowInsets.Type.ime()) != 0) {
-							imeAnimation = animation;
-							break;
-						}
-					}
-					// Update keyboard height based on IME animation.
-					if (imeAnimation != null) {
-						float interpolatedFraction = imeAnimation.getInterpolatedFraction();
-						// Linear interpolation between start and end values.
-						float keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction;
-						GodotLib.setVirtualKeyboardHeight((int)keyboardHeight);
-					}
-					return windowInsets;
-				}
-
-				@Override
-				public void onEnd(@NonNull WindowInsetsAnimation animation) {
-				}
-			});
-		} else {
-			// Infer the virtual keyboard height using visible area.
-			view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-				// Don't allocate a new Rect every time the callback is called.
-				final Rect visibleSize = new Rect();
-
-				@Override
-				public void onGlobalLayout() {
-					final SurfaceView view = mRenderView.getView();
-					view.getWindowVisibleDisplayFrame(visibleSize);
-					final int keyboardHeight = view.getHeight() - visibleSize.bottom;
-					GodotLib.setVirtualKeyboardHeight(keyboardHeight);
-				}
-			});
-		}
-
-		mRenderView.queueOnRenderThread(() -> {
-			for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-				plugin.onRegisterPluginWithGodotNative();
-			}
-			setKeepScreenOn(Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")));
-		});
-
-		// Include the returned non-null views in the Godot view hierarchy.
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			View pluginView = plugin.onMainCreate(activity);
-			if (pluginView != null) {
-				if (plugin.shouldBeOnTop()) {
-					containerLayout.addView(pluginView);
-				} else {
-					containerLayout.addView(pluginView, 0);
-				}
-			}
-		}
-		return true;
-	}
-
-	/**
-	 * Returns true if `Vulkan` is used for rendering.
-	 */
-	private boolean usesVulkan() {
-		final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method");
-		final String renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver");
-		return ("forward_plus".equals(renderer) || "mobile".equals(renderer)) && "vulkan".equals(renderingDevice);
-	}
-
-	/**
-	 * Returns true if the device meets the base requirements for Vulkan support, false otherwise.
-	 */
-	private boolean meetsVulkanRequirements(@Nullable PackageManager packageManager) {
-		if (packageManager == null) {
-			return false;
-		}
-
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-			if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) {
-				// Optional requirements.. log as warning if missing
-				Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1");
-			}
-
-			// Check for api version 1.0
-			return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003);
-		}
-
-		return false;
-	}
-
-	public void setKeepScreenOn(final boolean p_enabled) {
-		runOnUiThread(() -> {
-			if (p_enabled) {
-				getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-			} else {
-				getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-			}
-		});
-	}
-
-	/**
-	 * Used by the native code (java_godot_wrapper.h) to vibrate the device.
-	 * @param durationMs
-	 */
-	@SuppressLint("MissingPermission")
-	@Keep
-	private void vibrate(int durationMs) {
-		if (durationMs > 0 && requestPermission("VIBRATE")) {
-			Vibrator v = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
-			if (v != null) {
-				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-					v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE));
-				} else {
-					// deprecated in API 26
-					v.vibrate(durationMs);
-				}
-			}
-		}
-	}
-
-	public void restart() {
-		if (godotHost != null) {
-			godotHost.onGodotRestartRequested(this);
-		}
-	}
-
-	public void alert(final String message, final String title) {
-		alert(message, title, null);
-	}
-
-	private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) {
-		Resources res = getResources();
-		alert(res.getString(messageResId), res.getString(titleResId), okCallback);
-	}
-
-	private void alert(final String message, final String title, @Nullable Runnable okCallback) {
-		final Activity activity = getActivity();
-		runOnUiThread(() -> {
-			AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-			builder.setMessage(message).setTitle(title);
-			builder.setPositiveButton(
-					"OK",
-					(dialog, id) -> {
-						if (okCallback != null) {
-							okCallback.run();
-						}
-						dialog.cancel();
-					});
-			AlertDialog dialog = builder.create();
-			dialog.show();
-		});
-	}
-
-	public int getGLESVersionCode() {
-		ActivityManager am = (ActivityManager)getContext().getSystemService(Context.ACTIVITY_SERVICE);
-		ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo();
-		return deviceInfo.reqGlEsVersion;
-	}
-
-	@CallSuper
-	protected String[] getCommandLine() {
-		String[] original = parseCommandLine();
-		String[] updated;
-		List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null;
-		if (hostCommandLine == null || hostCommandLine.isEmpty()) {
-			updated = original;
-		} else {
-			updated = Arrays.copyOf(original, original.length + hostCommandLine.size());
-			for (int i = 0; i < hostCommandLine.size(); i++) {
-				updated[original.length + i] = hostCommandLine.get(i);
-			}
-		}
-		return updated;
-	}
-
-	private String[] parseCommandLine() {
-		InputStream is;
-		try {
-			is = getActivity().getAssets().open("_cl_");
-			byte[] len = new byte[4];
-			int r = is.read(len);
-			if (r < 4) {
-				return new String[0];
-			}
-			int argc = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF));
-			String[] cmdline = new String[argc];
-
-			for (int i = 0; i < argc; i++) {
-				r = is.read(len);
-				if (r < 4) {
-					return new String[0];
-				}
-				int strlen = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF));
-				if (strlen > 65535) {
-					return new String[0];
-				}
-				byte[] arg = new byte[strlen];
-				r = is.read(arg);
-				if (r == strlen) {
-					cmdline[i] = new String(arg, "UTF-8");
-				}
-			}
-			return cmdline;
-		} catch (Exception e) {
-			// The _cl_ file can be missing with no adverse effect
-			return new String[0];
-		}
-	}
-
-	/**
-	 * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused.
-	 */
-	@Keep
-	private boolean isActivityResumed() {
-		return activityResumed;
-	}
-
-	/**
-	 * Used by the native code (java_godot_wrapper.h) to access the Android surface.
-	 */
-	@Keep
-	private Surface getSurface() {
-		return mRenderView.getView().getHolder().getSurface();
-	}
-
-	/**
-	 * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
-	 * @return The input fallback mapping for the current XR mode.
-	 */
-	@Keep
-	private String getInputFallbackMapping() {
-		return xrMode.inputFallbackMapping;
-	}
-
-	String expansion_pack_path;
-
-	private void initializeGodot() {
-		if (expansion_pack_path != null) {
-			String[] new_cmdline;
-			int cll = 0;
-			if (command_line != null) {
-				new_cmdline = new String[command_line.length + 2];
-				cll = command_line.length;
-				for (int i = 0; i < command_line.length; i++) {
-					new_cmdline[i] = command_line[i];
-				}
-			} else {
-				new_cmdline = new String[2];
-			}
-
-			new_cmdline[cll] = "--main-pack";
-			new_cmdline[cll + 1] = expansion_pack_path;
-			command_line = new_cmdline;
-		}
-
-		final Activity activity = getActivity();
-		io = new GodotIO(activity);
-		netUtils = new GodotNetUtils(activity);
-		Context context = getContext();
-		directoryAccessHandler = new DirectoryAccessHandler(context);
-		fileAccessHandler = new FileAccessHandler(context);
-		mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
-		mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
-		mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
-		mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
-		mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
-
-		godot_initialized = GodotLib.initialize(activity,
-				this,
-				activity.getAssets(),
-				io,
-				netUtils,
-				directoryAccessHandler,
-				fileAccessHandler,
-				use_apk_expansion);
-
-		result_callback = null;
-	}
-
-	@Override
-	public void onServiceConnected(Messenger m) {
-		IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m);
-		remoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
-	}
-
-	@Override
-	public void onCreate(Bundle icicle) {
-		BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate");
-		super.onCreate(icicle);
-
-		final Activity activity = getActivity();
-		Window window = activity.getWindow();
-		window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
-		mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE);
-		pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this);
-
-		// check for apk expansion API
-		boolean md5mismatch = false;
-		command_line = getCommandLine();
-		String main_pack_md5 = null;
-		String main_pack_key = null;
-
-		List<String> new_args = new LinkedList<>();
-
-		for (int i = 0; i < command_line.length; i++) {
-			boolean has_extra = i < command_line.length - 1;
-			if (command_line[i].equals(XRMode.REGULAR.cmdLineArg)) {
-				xrMode = XRMode.REGULAR;
-			} else if (command_line[i].equals(XRMode.OPENXR.cmdLineArg)) {
-				xrMode = XRMode.OPENXR;
-			} else if (command_line[i].equals("--debug_opengl")) {
-				use_debug_opengl = true;
-			} else if (command_line[i].equals("--use_immersive")) {
-				use_immersive = true;
-				window.getDecorView().setSystemUiVisibility(
-						View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
-						View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-						View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-						View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
-						View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
-						View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-				UiChangeListener();
-			} else if (command_line[i].equals("--use_apk_expansion")) {
-				use_apk_expansion = true;
-			} else if (has_extra && command_line[i].equals("--apk_expansion_md5")) {
-				main_pack_md5 = command_line[i + 1];
-				i++;
-			} else if (has_extra && command_line[i].equals("--apk_expansion_key")) {
-				main_pack_key = command_line[i + 1];
-				SharedPreferences prefs = activity.getSharedPreferences("app_data_keys",
-						MODE_PRIVATE);
-				Editor editor = prefs.edit();
-				editor.putString("store_public_key", main_pack_key);
-
-				editor.apply();
-				i++;
-			} else if (command_line[i].equals("--benchmark")) {
-				BenchmarkUtils.setUseBenchmark(true);
-				new_args.add(command_line[i]);
-			} else if (has_extra && command_line[i].equals("--benchmark-file")) {
-				BenchmarkUtils.setUseBenchmark(true);
-				new_args.add(command_line[i]);
-
-				// Retrieve the filepath
-				BenchmarkUtils.setBenchmarkFile(command_line[i + 1]);
-				new_args.add(command_line[i + 1]);
-
-				i++;
-			} else if (command_line[i].trim().length() != 0) {
-				new_args.add(command_line[i]);
-			}
-		}
-
-		if (new_args.isEmpty()) {
-			command_line = null;
-		} else {
-			command_line = new_args.toArray(new String[new_args.size()]);
-		}
-		if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) {
-			// check that environment is ok!
-			if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-				// show popup and die
-			}
-
-			// Build the full path to the app's expansion files
-			try {
-				expansion_pack_path = Helpers.getSaveFilePath(getContext());
-				expansion_pack_path += "/main." + activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode + "." + activity.getPackageName() + ".obb";
-			} catch (Exception e) {
-				e.printStackTrace();
-			}
-
-			File f = new File(expansion_pack_path);
-
-			boolean pack_valid = true;
-
-			if (!f.exists()) {
-				pack_valid = false;
-
-			} else if (obbIsCorrupted(expansion_pack_path, main_pack_md5)) {
-				pack_valid = false;
-				try {
-					f.delete();
-				} catch (Exception e) {
-				}
-			}
-
-			if (!pack_valid) {
-				Intent notifierIntent = new Intent(activity, activity.getClass());
-				notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-				PendingIntent pendingIntent;
-				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-					pendingIntent = PendingIntent.getActivity(activity, 0,
-							notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-				} else {
-					pendingIntent = PendingIntent.getActivity(activity, 0,
-							notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-				}
-
-				int startResult;
-				try {
-					startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(
-							getContext(),
-							pendingIntent,
-							GodotDownloaderService.class);
-
-					if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
-						// This is where you do set up to display the download
-						// progress (next step in onCreateView)
-						mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
-								GodotDownloaderService.class);
-
-						return;
-					}
-				} catch (NameNotFoundException e) {
-					// TODO Auto-generated catch block
-				}
-			}
-		}
-
-		mCurrentIntent = activity.getIntent();
-
-		initializeGodot();
-		BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate");
-	}
-
-	@Override
-	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
-		if (mDownloaderClientStub != null) {
-			View downloadingExpansionView =
-					inflater.inflate(R.layout.downloading_expansion, container, false);
-			mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar);
-			mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText);
-			mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction);
-			mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage);
-			mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed);
-			mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining);
-			mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
-			mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
-			mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
-			mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton);
-
-			return downloadingExpansionView;
-		}
-
-		return containerLayout;
-	}
-
-	@Override
-	public void onDestroy() {
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onMainDestroy();
-		}
-
-		GodotLib.ondestroy();
-
-		super.onDestroy();
-
-		forceQuit();
-	}
-
-	@Override
-	public void onPause() {
-		super.onPause();
-		activityResumed = false;
-
-		if (!godot_initialized) {
-			if (null != mDownloaderClientStub) {
-				mDownloaderClientStub.disconnect(getActivity());
-			}
-			return;
-		}
-		mRenderView.onActivityPaused();
-
-		mSensorManager.unregisterListener(this);
-
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onMainPause();
-		}
-	}
-
-	public boolean hasClipboard() {
-		return mClipboard.hasPrimaryClip();
-	}
-
-	public String getClipboard() {
-		ClipData clipData = mClipboard.getPrimaryClip();
-		if (clipData == null)
-			return "";
-		CharSequence text = clipData.getItemAt(0).getText();
-		if (text == null)
-			return "";
-		return text.toString();
-	}
-
-	public void setClipboard(String p_text) {
-		ClipData clip = ClipData.newPlainText("myLabel", p_text);
-		mClipboard.setPrimaryClip(clip);
-	}
-
-	@Override
-	public void onResume() {
-		super.onResume();
-		activityResumed = true;
-		if (!godot_initialized) {
-			if (null != mDownloaderClientStub) {
-				mDownloaderClientStub.connect(getActivity());
-			}
-			return;
-		}
-
-		mRenderView.onActivityResumed();
-
-		mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
-		mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME);
-		mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
-		mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME);
-
-		if (use_immersive) {
-			Window window = getActivity().getWindow();
-			window.getDecorView().setSystemUiVisibility(
-					View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
-					View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-					View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-					View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
-					View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
-					View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-		}
-
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			plugin.onMainResume();
-		}
-	}
-
-	public void UiChangeListener() {
-		final View decorView = getActivity().getWindow().getDecorView();
-		decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
-			if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
-				decorView.setSystemUiVisibility(
-						View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
-						View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-						View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-						View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
-						View.SYSTEM_UI_FLAG_FULLSCREEN |
-						View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-			}
-		});
-	}
-
-	public float[] getRotatedValues(float values[]) {
-		if (values == null || values.length != 3) {
-			return values;
-		}
-
-		Display display =
-				((WindowManager)getActivity().getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
-		int displayRotation = display.getRotation();
-
-		float[] rotatedValues = new float[3];
-		switch (displayRotation) {
-			case Surface.ROTATION_0:
-				rotatedValues[0] = values[0];
-				rotatedValues[1] = values[1];
-				rotatedValues[2] = values[2];
-				break;
-			case Surface.ROTATION_90:
-				rotatedValues[0] = -values[1];
-				rotatedValues[1] = values[0];
-				rotatedValues[2] = values[2];
-				break;
-			case Surface.ROTATION_180:
-				rotatedValues[0] = -values[0];
-				rotatedValues[1] = -values[1];
-				rotatedValues[2] = values[2];
-				break;
-			case Surface.ROTATION_270:
-				rotatedValues[0] = values[1];
-				rotatedValues[1] = -values[0];
-				rotatedValues[2] = values[2];
-				break;
-		}
-
-		return rotatedValues;
-	}
-
-	@Override
-	public void onSensorChanged(SensorEvent event) {
-		if (mRenderView == null) {
-			return;
-		}
-
-		final int typeOfSensor = event.sensor.getType();
-		switch (typeOfSensor) {
-			case Sensor.TYPE_ACCELEROMETER: {
-				float[] rotatedValues = getRotatedValues(event.values);
-				mRenderView.queueOnRenderThread(() -> {
-					GodotLib.accelerometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
-				});
-				break;
-			}
-			case Sensor.TYPE_GRAVITY: {
-				float[] rotatedValues = getRotatedValues(event.values);
-				mRenderView.queueOnRenderThread(() -> {
-					GodotLib.gravity(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
-				});
-				break;
-			}
-			case Sensor.TYPE_MAGNETIC_FIELD: {
-				float[] rotatedValues = getRotatedValues(event.values);
-				mRenderView.queueOnRenderThread(() -> {
-					GodotLib.magnetometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
-				});
-				break;
-			}
-			case Sensor.TYPE_GYROSCOPE: {
-				float[] rotatedValues = getRotatedValues(event.values);
-				mRenderView.queueOnRenderThread(() -> {
-					GodotLib.gyroscope(rotatedValues[0], rotatedValues[1], rotatedValues[2]);
-				});
-				break;
-			}
-		}
-	}
-
-	@Override
-	public final void onAccuracyChanged(Sensor sensor, int accuracy) {
-		// Do something here if sensor accuracy changes.
-	}
-
-	public void onBackPressed() {
-		boolean shouldQuit = true;
-
-		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
-			if (plugin.onMainBackPressed()) {
-				shouldQuit = false;
-			}
-		}
-
-		if (shouldQuit && mRenderView != null) {
-			mRenderView.queueOnRenderThread(GodotLib::back);
-		}
-	}
-
-	/**
-	 * Queue a runnable to be run on the render thread.
-	 * <p>
-	 * This must be called after the render thread has started.
-	 */
-	public final void runOnRenderThread(@NonNull Runnable action) {
-		if (mRenderView != null) {
-			mRenderView.queueOnRenderThread(action);
-		}
-	}
-
-	public final void runOnUiThread(@NonNull Runnable action) {
-		if (getActivity() != null) {
-			getActivity().runOnUiThread(action);
-		}
-	}
-
-	private void forceQuit() {
-		// TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each
-		// native Godot components that is started in Godot#onVideoInit.
-		forceQuit(0);
-	}
-
-	@Keep
-	private boolean forceQuit(int instanceId) {
-		if (godotHost == null) {
-			return false;
-		}
-		if (instanceId == 0) {
-			godotHost.onGodotForceQuit(this);
-			return true;
-		} else {
-			return godotHost.onGodotForceQuit(instanceId);
-		}
-	}
-
-	private boolean obbIsCorrupted(String f, String main_pack_md5) {
-		try {
-			InputStream fis = new FileInputStream(f);
-
-			// Create MD5 Hash
-			byte[] buffer = new byte[16384];
-
-			MessageDigest complete = MessageDigest.getInstance("MD5");
-			int numRead;
-			do {
-				numRead = fis.read(buffer);
-				if (numRead > 0) {
-					complete.update(buffer, 0, numRead);
-				}
-			} while (numRead != -1);
-
-			fis.close();
-			byte[] messageDigest = complete.digest();
-
-			// Create Hex String
-			StringBuilder hexString = new StringBuilder();
-			for (byte b : messageDigest) {
-				String s = Integer.toHexString(0xFF & b);
-				if (s.length() == 1) {
-					s = "0" + s;
-				}
-				hexString.append(s);
-			}
-			String md5str = hexString.toString();
-
-			if (!md5str.equals(main_pack_md5)) {
-				return true;
-			}
-			return false;
-		} catch (Exception e) {
-			e.printStackTrace();
-			return true;
-		}
-	}
-
-	public boolean requestPermission(String p_name) {
-		return PermissionsUtil.requestPermission(p_name, getActivity());
-	}
-
-	public boolean requestPermissions() {
-		return PermissionsUtil.requestManifestPermissions(getActivity());
-	}
-
-	public String[] getGrantedPermissions() {
-		return PermissionsUtil.getGrantedPermissions(getActivity());
-	}
-
-	@Keep
-	private String getCACertificates() {
-		return GodotNetUtils.getCACertificates();
-	}
-
-	/**
-	 * The download state should trigger changes in the UI --- it may be useful
-	 * to show the state as being indeterminate at times. This sample can be
-	 * considered a guideline.
-	 */
-	@Override
-	public void onDownloadStateChanged(int newState) {
-		setState(newState);
-		boolean showDashboard = true;
-		boolean showCellMessage = false;
-		boolean paused;
-		boolean indeterminate;
-		switch (newState) {
-			case IDownloaderClient.STATE_IDLE:
-				// STATE_IDLE means the service is listening, so it's
-				// safe to start making remote service calls.
-				paused = false;
-				indeterminate = true;
-				break;
-			case IDownloaderClient.STATE_CONNECTING:
-			case IDownloaderClient.STATE_FETCHING_URL:
-				showDashboard = true;
-				paused = false;
-				indeterminate = true;
-				break;
-			case IDownloaderClient.STATE_DOWNLOADING:
-				paused = false;
-				showDashboard = true;
-				indeterminate = false;
-				break;
-
-			case IDownloaderClient.STATE_FAILED_CANCELED:
-			case IDownloaderClient.STATE_FAILED:
-			case IDownloaderClient.STATE_FAILED_FETCHING_URL:
-			case IDownloaderClient.STATE_FAILED_UNLICENSED:
-				paused = true;
-				showDashboard = false;
-				indeterminate = false;
-				break;
-			case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
-			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
-				showDashboard = false;
-				paused = true;
-				indeterminate = false;
-				showCellMessage = true;
-				break;
-
-			case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
-				paused = true;
-				indeterminate = false;
-				break;
-			case IDownloaderClient.STATE_PAUSED_ROAMING:
-			case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
-				paused = true;
-				indeterminate = false;
-				break;
-			case IDownloaderClient.STATE_COMPLETED:
-				showDashboard = false;
-				paused = false;
-				indeterminate = false;
-				initializeGodot();
-				return;
-			default:
-				paused = true;
-				indeterminate = true;
-				showDashboard = true;
-		}
-		int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
-		if (mDashboard.getVisibility() != newDashboardVisibility) {
-			mDashboard.setVisibility(newDashboardVisibility);
-		}
-		int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
-		if (mCellMessage.getVisibility() != cellMessageVisibility) {
-			mCellMessage.setVisibility(cellMessageVisibility);
-		}
-
-		mPB.setIndeterminate(indeterminate);
-		setButtonPausedState(paused);
-	}
-
-	@Override
-	public void onDownloadProgress(DownloadProgressInfo progress) {
-		mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
-				Helpers.getSpeedString(progress.mCurrentSpeed)));
-		mTimeRemaining.setText(getString(R.string.time_remaining,
-				Helpers.getTimeRemaining(progress.mTimeRemaining)));
-
-		mPB.setMax((int)(progress.mOverallTotal >> 8));
-		mPB.setProgress((int)(progress.mOverallProgress >> 8));
-		mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal));
-		mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
-				progress.mOverallTotal));
-	}
-
-	public void initInputDevices() {
-		mRenderView.initInputDevices();
-	}
-
-	@Keep
-	public GodotRenderView getRenderView() { // used by native side to get renderView
-		return mRenderView;
-	}
-
-	@Keep
-	public DirectoryAccessHandler getDirectoryAccessHandler() {
-		return directoryAccessHandler;
-	}
-
-	@Keep
-	public FileAccessHandler getFileAccessHandler() {
-		return fileAccessHandler;
-	}
-
-	@Keep
-	private int createNewGodotInstance(String[] args) {
-		if (godotHost != null) {
-			return godotHost.onNewGodotInstanceRequested(args);
-		}
-		return 0;
-	}
-
-	@Keep
-	private void beginBenchmarkMeasure(String label) {
-		BenchmarkUtils.beginBenchmarkMeasure(label);
-	}
-
-	@Keep
-	private void endBenchmarkMeasure(String label) {
-		BenchmarkUtils.endBenchmarkMeasure(label);
-	}
-
-	@Keep
-	private void dumpBenchmark(String benchmarkFile) {
-		BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile);
-	}
-}

+ 965 - 0
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -0,0 +1,965 @@
+/**************************************************************************/
+/*  Godot.kt                                                              */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.*
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.graphics.Rect
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.os.*
+import android.util.Log
+import android.view.*
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.widget.FrameLayout
+import androidx.annotation.Keep
+import androidx.annotation.StringRes
+import com.google.android.vending.expansion.downloader.*
+import org.godotengine.godot.input.GodotEditText
+import org.godotengine.godot.io.directory.DirectoryAccessHandler
+import org.godotengine.godot.io.file.FileAccessHandler
+import org.godotengine.godot.plugin.GodotPluginRegistry
+import org.godotengine.godot.tts.GodotTTS
+import org.godotengine.godot.utils.GodotNetUtils
+import org.godotengine.godot.utils.PermissionsUtil
+import org.godotengine.godot.utils.PermissionsUtil.requestPermission
+import org.godotengine.godot.utils.beginBenchmarkMeasure
+import org.godotengine.godot.utils.benchmarkFile
+import org.godotengine.godot.utils.dumpBenchmark
+import org.godotengine.godot.utils.endBenchmarkMeasure
+import org.godotengine.godot.utils.useBenchmark
+import org.godotengine.godot.xr.XRMode
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.*
+
+/**
+ * Core component used to interface with the native layer of the engine.
+ *
+ * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
+ * lifecycle methods are properly invoked.
+ */
+class Godot(private val context: Context) : SensorEventListener {
+
+	private companion object {
+		private val TAG = Godot::class.java.simpleName
+	}
+
+	private val pluginRegistry: GodotPluginRegistry by lazy {
+		GodotPluginRegistry.initializePluginRegistry(this)
+	}
+	private val mSensorManager: SensorManager by lazy {
+		requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager
+	}
+	private val mAccelerometer: Sensor by lazy {
+		mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+	}
+	private val mGravity: Sensor by lazy {
+		mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
+	}
+	private val mMagnetometer: Sensor by lazy {
+		mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
+	}
+	private val mGyroscope: Sensor by lazy {
+		mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+	}
+	private val mClipboard: ClipboardManager by lazy {
+		requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+	}
+
+	private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int ->
+		if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
+			val decorView = requireActivity().window.decorView
+			decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+					View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+					View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+					View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
+					View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+		}}
+
+	val tts = GodotTTS(context)
+	val directoryAccessHandler = DirectoryAccessHandler(context)
+	val fileAccessHandler = FileAccessHandler(context)
+	val netUtils = GodotNetUtils(context)
+
+	/**
+	 * Tracks whether [onCreate] was completed successfully.
+	 */
+	private var initializationStarted = false
+
+	/**
+	 * Tracks whether [GodotLib.initialize] was completed successfully.
+	 */
+	private var nativeLayerInitializeCompleted = false
+
+	/**
+	 * Tracks whether [GodotLib.setup] was completed successfully.
+	 */
+	private var nativeLayerSetupCompleted = false
+
+	/**
+	 * Tracks whether [onInitRenderView] was completed successfully.
+	 */
+	private var renderViewInitialized = false
+	private var primaryHost: GodotHost? = null
+
+	var io: GodotIO? = null
+
+	private var commandLine : MutableList<String> = ArrayList<String>()
+	private var xrMode = XRMode.REGULAR
+	private var expansionPackPath: String = ""
+	private var useApkExpansion = false
+	private var useImmersive = false
+	private var useDebugOpengl = false
+
+	private var containerLayout: FrameLayout? = null
+	var renderView: GodotRenderView? = null
+
+	/**
+	 * Returns true if the native engine has been initialized through [onInitNativeLayer], false otherwise.
+	 */
+	private fun isNativeInitialized() = nativeLayerInitializeCompleted && nativeLayerSetupCompleted
+
+	/**
+	 * Returns true if the engine has been initialized, false otherwise.
+	 */
+	fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized
+
+	/**
+	 * Provides access to the primary host [Activity]
+	 */
+	fun getActivity() = primaryHost?.activity
+	private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
+
+	/**
+	 * Start initialization of the Godot engine.
+	 *
+	 * This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete
+	 * initialization of the engine.
+	 *
+	 * @throws IllegalArgumentException exception if the specified expansion pack (if any)
+	 * is invalid.
+	 */
+	fun onCreate(primaryHost: GodotHost) {
+		if (this.primaryHost != null || initializationStarted) {
+			Log.d(TAG, "OnCreate already invoked")
+			return
+		}
+
+		beginBenchmarkMeasure("Godot::onCreate")
+		try {
+			this.primaryHost = primaryHost
+			val activity = requireActivity()
+			val window = activity.window
+			window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
+			GodotPluginRegistry.initializePluginRegistry(this)
+			if (io == null) {
+				io = GodotIO(activity)
+			}
+
+			// check for apk expansion API
+			commandLine = getCommandLine()
+			var mainPackMd5: String? = null
+			var mainPackKey: String? = null
+			val newArgs: MutableList<String> = ArrayList()
+			var i = 0
+			while (i < commandLine.size) {
+				val hasExtra: Boolean = i < commandLine.size - 1
+				if (commandLine[i] == XRMode.REGULAR.cmdLineArg) {
+					xrMode = XRMode.REGULAR
+				} else if (commandLine[i] == XRMode.OPENXR.cmdLineArg) {
+					xrMode = XRMode.OPENXR
+				} else if (commandLine[i] == "--debug_opengl") {
+					useDebugOpengl = true
+				} else if (commandLine[i] == "--use_immersive") {
+					useImmersive = true
+					window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+							View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+							View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+							View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or  // hide nav bar
+							View.SYSTEM_UI_FLAG_FULLSCREEN or  // hide status bar
+							View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+					registerUiChangeListener()
+				} else if (commandLine[i] == "--use_apk_expansion") {
+					useApkExpansion = true
+				} else if (hasExtra && commandLine[i] == "--apk_expansion_md5") {
+					mainPackMd5 = commandLine[i + 1]
+					i++
+				} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
+					mainPackKey = commandLine[i + 1]
+					val prefs = activity.getSharedPreferences(
+							"app_data_keys",
+							Context.MODE_PRIVATE
+					)
+					val editor = prefs.edit()
+					editor.putString("store_public_key", mainPackKey)
+					editor.apply()
+					i++
+				} else if (commandLine[i] == "--benchmark") {
+					useBenchmark = true
+					newArgs.add(commandLine[i])
+				} else if (hasExtra && commandLine[i] == "--benchmark-file") {
+					useBenchmark = true
+					newArgs.add(commandLine[i])
+
+					// Retrieve the filepath
+					benchmarkFile = commandLine[i + 1]
+					newArgs.add(commandLine[i + 1])
+
+					i++
+				} else if (commandLine[i].trim().isNotEmpty()) {
+					newArgs.add(commandLine[i])
+				}
+				i++
+			}
+			if (newArgs.isEmpty()) {
+				commandLine = mutableListOf()
+			} else {
+				commandLine = newArgs
+			}
+			if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
+				// Build the full path to the app's expansion files
+				try {
+					expansionPackPath = Helpers.getSaveFilePath(context)
+					expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
+							activity.packageName,
+							0
+					).versionCode + "." + activity.packageName + ".obb"
+				} catch (e: java.lang.Exception) {
+					Log.e(TAG, "Unable to build full path to the app's expansion files", e)
+				}
+				val f = File(expansionPackPath)
+				var packValid = true
+				if (!f.exists()) {
+					packValid = false
+				} else if (obbIsCorrupted(expansionPackPath, mainPackMd5)) {
+					packValid = false
+					try {
+						f.delete()
+					} catch (_: java.lang.Exception) {
+					}
+				}
+				if (!packValid) {
+					// Aborting engine initialization
+					throw IllegalArgumentException("Invalid expansion pack")
+				}
+			}
+
+			initializationStarted = true
+		} catch (e: java.lang.Exception) {
+			// Clear the primary host and rethrow
+			this.primaryHost = null
+			initializationStarted = false
+			throw e
+		} finally {
+			endBenchmarkMeasure("Godot::onCreate");
+		}
+	}
+
+	/**
+	 * Initializes the native layer of the Godot engine.
+	 *
+	 * This must be preceded by [onCreate] and followed by [onInitRenderView] to complete
+	 * initialization of the engine.
+	 *
+	 * @return false if initialization of the native layer fails, true otherwise.
+	 *
+	 * @throws IllegalStateException if [onCreate] has not been called.
+	 */
+	fun onInitNativeLayer(host: GodotHost): Boolean {
+		if (!initializationStarted) {
+			throw IllegalStateException("OnCreate must be invoked successfully prior to initializing the native layer")
+		}
+		if (isNativeInitialized()) {
+			Log.d(TAG, "OnInitNativeLayer already invoked")
+			return true
+		}
+		if (host != primaryHost) {
+			Log.e(TAG, "Native initialization is only supported for the primary host")
+			return false
+		}
+
+		if (expansionPackPath.isNotEmpty()) {
+			commandLine.add("--main-pack")
+			commandLine.add(expansionPackPath)
+		}
+		val activity = requireActivity()
+		if (!nativeLayerInitializeCompleted) {
+			nativeLayerInitializeCompleted = GodotLib.initialize(
+					activity,
+					this,
+					activity.assets,
+					io,
+					netUtils,
+					directoryAccessHandler,
+					fileAccessHandler,
+					useApkExpansion,
+			)
+		}
+
+		if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
+			nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
+			if (!nativeLayerSetupCompleted) {
+				Log.e(TAG, "Unable to setup the Godot engine! Aborting...")
+				alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit)
+			}
+		}
+		return isNativeInitialized()
+	}
+
+	/**
+	 * Used to complete initialization of the view used by the engine for rendering.
+	 *
+	 * This must be preceded by [onCreate] and [onInitNativeLayer] in that order to properly
+	 * initialize the engine.
+	 *
+	 * @param host The [GodotHost] that's initializing the render views
+	 * @param providedContainerLayout Optional argument; if provided, this is reused to host the Godot's render views
+	 *
+	 * @return A [FrameLayout] instance containing Godot's render views if initialization is successful, null otherwise.
+	 *
+	 * @throws IllegalStateException if [onInitNativeLayer] has not been called
+	 */
+	@JvmOverloads
+	fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? {
+		if (!isNativeInitialized()) {
+			throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view")
+		}
+
+		try {
+			val activity: Activity = host.activity
+			containerLayout = providedContainerLayout
+			containerLayout?.removeAllViews()
+			containerLayout?.layoutParams = ViewGroup.LayoutParams(
+					ViewGroup.LayoutParams.MATCH_PARENT,
+					ViewGroup.LayoutParams.MATCH_PARENT
+			)
+
+			// GodotEditText layout
+			val editText = GodotEditText(activity)
+			editText.layoutParams =
+					ViewGroup.LayoutParams(
+							ViewGroup.LayoutParams.MATCH_PARENT,
+							activity.resources.getDimension(R.dimen.text_edit_height).toInt()
+					)
+			// ...add to FrameLayout
+			containerLayout?.addView(editText)
+			renderView = if (usesVulkan()) {
+				if (!meetsVulkanRequirements(activity.packageManager)) {
+					alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit)
+					return null
+				}
+				GodotVulkanRenderView(host, this)
+			} else {
+				// Fallback to openGl
+				GodotGLRenderView(host, this, xrMode, useDebugOpengl)
+			}
+			if (host == primaryHost) {
+				renderView!!.startRenderer()
+			}
+			val view: View = renderView!!.view
+			containerLayout?.addView(
+					view,
+					ViewGroup.LayoutParams(
+							ViewGroup.LayoutParams.MATCH_PARENT,
+							ViewGroup.LayoutParams.MATCH_PARENT
+					)
+			)
+			editText.setView(renderView)
+			io?.setEdit(editText)
+
+			// Listeners for keyboard height.
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+				// Report the height of virtual keyboard as it changes during the animation.
+				val decorView = activity.window.decorView
+				decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
+					var startBottom = 0
+					var endBottom = 0
+					override fun onPrepare(animation: WindowInsetsAnimation) {
+						startBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom
+					}
+
+					override fun onStart(animation: WindowInsetsAnimation, bounds: WindowInsetsAnimation.Bounds): WindowInsetsAnimation.Bounds {
+						endBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom
+						return bounds
+					}
+
+					override fun onProgress(windowInsets: WindowInsets, list: List<WindowInsetsAnimation>): WindowInsets {
+						// Find the IME animation.
+						var imeAnimation: WindowInsetsAnimation? = null
+						for (animation in list) {
+							if (animation.typeMask and WindowInsets.Type.ime() != 0) {
+								imeAnimation = animation
+								break
+							}
+						}
+						// Update keyboard height based on IME animation.
+						if (imeAnimation != null) {
+							val interpolatedFraction = imeAnimation.interpolatedFraction
+							// Linear interpolation between start and end values.
+							val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction
+							GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt())
+						}
+						return windowInsets
+					}
+
+					override fun onEnd(animation: WindowInsetsAnimation) {}
+				})
+			} else {
+				// Infer the virtual keyboard height using visible area.
+				view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+					// Don't allocate a new Rect every time the callback is called.
+					val visibleSize = Rect()
+					override fun onGlobalLayout() {
+						val surfaceView = renderView!!.view
+						surfaceView.getWindowVisibleDisplayFrame(visibleSize)
+						val keyboardHeight = surfaceView.height - visibleSize.bottom
+						GodotLib.setVirtualKeyboardHeight(keyboardHeight)
+					}
+				})
+			}
+
+			if (host == primaryHost) {
+				renderView!!.queueOnRenderThread {
+					for (plugin in pluginRegistry.allPlugins) {
+						plugin.onRegisterPluginWithGodotNative()
+					}
+					setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
+				}
+
+				// Include the returned non-null views in the Godot view hierarchy.
+				for (plugin in pluginRegistry.allPlugins) {
+					val pluginView = plugin.onMainCreate(activity)
+					if (pluginView != null) {
+						if (plugin.shouldBeOnTop()) {
+							containerLayout?.addView(pluginView)
+						} else {
+							containerLayout?.addView(pluginView, 0)
+						}
+					}
+				}
+			}
+			renderViewInitialized = true
+		} finally {
+			if (!renderViewInitialized) {
+				containerLayout?.removeAllViews()
+				containerLayout = null
+			}
+		}
+		return containerLayout
+	}
+
+	fun onResume(host: GodotHost) {
+		if (host != primaryHost) {
+			return
+		}
+
+		renderView!!.onActivityResumed()
+		mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
+		mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME)
+		mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
+		mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
+		if (useImmersive) {
+			val window = requireActivity().window
+			window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+					View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+					View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+					View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or  // hide nav bar
+					View.SYSTEM_UI_FLAG_FULLSCREEN or  // hide status bar
+					View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+		}
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onMainResume()
+		}
+	}
+
+	fun onPause(host: GodotHost) {
+		if (host != primaryHost) {
+			return
+		}
+
+		renderView!!.onActivityPaused()
+		mSensorManager.unregisterListener(this)
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onMainPause()
+		}
+	}
+
+	fun onDestroy(primaryHost: GodotHost) {
+		if (this.primaryHost != primaryHost) {
+			return
+		}
+
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onMainDestroy()
+		}
+		GodotLib.ondestroy()
+		forceQuit()
+	}
+
+	/**
+	 * Activity result callback
+	 */
+	fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onMainActivityResult(requestCode, resultCode, data)
+		}
+	}
+
+	/**
+	 * Permissions request callback
+	 */
+	fun onRequestPermissionsResult(
+		requestCode: Int,
+		permissions: Array<String?>,
+		grantResults: IntArray
+	) {
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults)
+		}
+		for (i in permissions.indices) {
+			GodotLib.requestPermissionResult(
+				permissions[i],
+				grantResults[i] == PackageManager.PERMISSION_GRANTED
+			)
+		}
+	}
+
+	/**
+	 * Invoked on the render thread when the Godot setup is complete.
+	 */
+	private fun onGodotSetupCompleted() {
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onGodotSetupCompleted()
+		}
+		primaryHost?.onGodotSetupCompleted()
+	}
+
+	/**
+	 * Invoked on the render thread when the Godot main loop has started.
+	 */
+	private fun onGodotMainLoopStarted() {
+		for (plugin in pluginRegistry.allPlugins) {
+			plugin.onGodotMainLoopStarted()
+		}
+		primaryHost?.onGodotMainLoopStarted()
+	}
+
+	private fun restart() {
+		primaryHost?.onGodotRestartRequested(this)
+	}
+
+	private fun registerUiChangeListener() {
+		val decorView = requireActivity().window.decorView
+		decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener)
+	}
+
+	@Keep
+	private fun alert(message: String, title: String) {
+		alert(message, title, null)
+	}
+
+	private fun alert(
+		@StringRes messageResId: Int,
+		@StringRes titleResId: Int,
+		okCallback: Runnable?
+	) {
+		val res: Resources = getActivity()?.resources ?: return
+		alert(res.getString(messageResId), res.getString(titleResId), okCallback)
+	}
+
+	private fun alert(message: String, title: String, okCallback: Runnable?) {
+		val activity: Activity = getActivity() ?: return
+		runOnUiThread(Runnable {
+			val builder = AlertDialog.Builder(activity)
+			builder.setMessage(message).setTitle(title)
+			builder.setPositiveButton(
+				"OK"
+			) { dialog: DialogInterface, id: Int ->
+				okCallback?.run()
+				dialog.cancel()
+			}
+			val dialog = builder.create()
+			dialog.show()
+		})
+	}
+
+	/**
+	 * Queue a runnable to be run on the render thread.
+	 *
+	 * This must be called after the render thread has started.
+	 */
+	fun runOnRenderThread(action: Runnable) {
+		if (renderView != null) {
+			renderView!!.queueOnRenderThread(action)
+		}
+	}
+
+	/**
+	 * Runs the specified action on the UI thread.
+	 * If the current thread is the UI thread, then the action is executed immediately.
+	 * If the current thread is not the UI thread, the action is posted to the event queue
+	 * of the UI thread.
+	 */
+	fun runOnUiThread(action: Runnable) {
+		val activity: Activity = getActivity() ?: return
+		activity.runOnUiThread(action)
+	}
+
+	/**
+	 * Returns true if the call is being made on the Ui thread.
+	 */
+	private fun isOnUiThread() = Looper.myLooper() == Looper.getMainLooper()
+
+	/**
+	 * Returns true if `Vulkan` is used for rendering.
+	 */
+	private fun usesVulkan(): Boolean {
+		val renderer = GodotLib.getGlobal("rendering/renderer/rendering_method")
+		val renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver")
+		return ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice
+	}
+
+	/**
+	 * Returns true if the device meets the base requirements for Vulkan support, false otherwise.
+	 */
+	private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean {
+		if (packageManager == null) {
+			return false
+		}
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+			if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) {
+				// Optional requirements.. log as warning if missing
+				Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1")
+			}
+
+			// Check for api version 1.0
+			return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003)
+		}
+		return false
+	}
+
+	private fun setKeepScreenOn(p_enabled: Boolean) {
+		runOnUiThread {
+			if (p_enabled) {
+				getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+			} else {
+				getActivity()?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+			}
+		}
+	}
+
+	fun hasClipboard(): Boolean {
+		return mClipboard.hasPrimaryClip()
+	}
+
+	fun getClipboard(): String? {
+		val clipData = mClipboard.primaryClip ?: return ""
+		val text = clipData.getItemAt(0).text ?: return ""
+		return text.toString()
+	}
+
+	fun setClipboard(text: String?) {
+		val clip = ClipData.newPlainText("myLabel", text)
+		mClipboard.setPrimaryClip(clip)
+	}
+
+	private fun forceQuit() {
+		forceQuit(0)
+	}
+
+	@Keep
+	private fun forceQuit(instanceId: Int): Boolean {
+		if (primaryHost == null) {
+			return false
+		}
+		return if (instanceId == 0) {
+			primaryHost!!.onGodotForceQuit(this)
+			true
+		} else {
+			primaryHost!!.onGodotForceQuit(instanceId)
+		}
+	}
+
+	fun onBackPressed(host: GodotHost) {
+		if (host != primaryHost) {
+			return
+		}
+
+		var shouldQuit = true
+		for (plugin in pluginRegistry.allPlugins) {
+			if (plugin.onMainBackPressed()) {
+				shouldQuit = false
+			}
+		}
+		if (shouldQuit && renderView != null) {
+			renderView!!.queueOnRenderThread { GodotLib.back() }
+		}
+	}
+
+	private fun getRotatedValues(values: FloatArray?): FloatArray? {
+		if (values == null || values.size != 3) {
+			return values
+		}
+		val display =
+			(requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
+		val displayRotation = display.rotation
+		val rotatedValues = FloatArray(3)
+		when (displayRotation) {
+			Surface.ROTATION_0 -> {
+				rotatedValues[0] = values[0]
+				rotatedValues[1] = values[1]
+				rotatedValues[2] = values[2]
+			}
+			Surface.ROTATION_90 -> {
+				rotatedValues[0] = -values[1]
+				rotatedValues[1] = values[0]
+				rotatedValues[2] = values[2]
+			}
+			Surface.ROTATION_180 -> {
+				rotatedValues[0] = -values[0]
+				rotatedValues[1] = -values[1]
+				rotatedValues[2] = values[2]
+			}
+			Surface.ROTATION_270 -> {
+				rotatedValues[0] = values[1]
+				rotatedValues[1] = -values[0]
+				rotatedValues[2] = values[2]
+			}
+		}
+		return rotatedValues
+	}
+
+	override fun onSensorChanged(event: SensorEvent) {
+		if (renderView == null) {
+			return
+		}
+		when (event.sensor.type) {
+			Sensor.TYPE_ACCELEROMETER -> {
+				val rotatedValues = getRotatedValues(event.values)
+				renderView!!.queueOnRenderThread {
+					GodotLib.accelerometer(
+						-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
+					)
+				}
+			}
+			Sensor.TYPE_GRAVITY -> {
+				val rotatedValues = getRotatedValues(event.values)
+				renderView!!.queueOnRenderThread {
+					GodotLib.gravity(
+						-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
+					)
+				}
+			}
+			Sensor.TYPE_MAGNETIC_FIELD -> {
+				val rotatedValues = getRotatedValues(event.values)
+				renderView!!.queueOnRenderThread {
+					GodotLib.magnetometer(
+						-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
+					)
+				}
+			}
+			Sensor.TYPE_GYROSCOPE -> {
+				val rotatedValues = getRotatedValues(event.values)
+				renderView!!.queueOnRenderThread {
+					GodotLib.gyroscope(
+						rotatedValues!![0], rotatedValues[1], rotatedValues[2]
+					)
+				}
+			}
+		}
+	}
+
+	override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+		// Do something here if sensor accuracy changes.
+	}
+
+	/**
+	 * Used by the native code (java_godot_wrapper.h) to vibrate the device.
+	 * @param durationMs
+	 */
+	@SuppressLint("MissingPermission")
+	@Keep
+	private fun vibrate(durationMs: Int) {
+		if (durationMs > 0 && requestPermission("VIBRATE")) {
+			val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+				vibratorService.vibrate(
+					VibrationEffect.createOneShot(
+						durationMs.toLong(),
+						VibrationEffect.DEFAULT_AMPLITUDE
+					)
+				)
+			} else {
+				// deprecated in API 26
+				vibratorService.vibrate(durationMs.toLong())
+			}
+		}
+	}
+
+	private fun getCommandLine(): MutableList<String> {
+		val original: MutableList<String> = parseCommandLine()
+		val hostCommandLine = primaryHost?.commandLine
+		if (hostCommandLine != null && hostCommandLine.isNotEmpty()) {
+			original.addAll(hostCommandLine)
+		}
+		return original
+	}
+
+	private fun parseCommandLine(): MutableList<String> {
+		val inputStream: InputStream
+		return try {
+			inputStream = requireActivity().assets.open("_cl_")
+			val len = ByteArray(4)
+			var r = inputStream.read(len)
+			if (r < 4) {
+				return mutableListOf()
+			}
+			val argc =
+				(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
+			val cmdline = ArrayList<String>(argc)
+			for (i in 0 until argc) {
+				r = inputStream.read(len)
+				if (r < 4) {
+					return mutableListOf()
+				}
+				val strlen =
+					(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
+				if (strlen > 65535) {
+					return mutableListOf()
+				}
+				val arg = ByteArray(strlen)
+				r = inputStream.read(arg)
+				if (r == strlen) {
+					cmdline[i] = String(arg, StandardCharsets.UTF_8)
+				}
+			}
+			cmdline
+		} catch (e: Exception) {
+			// The _cl_ file can be missing with no adverse effect
+			mutableListOf()
+		}
+	}
+
+	/**
+	 * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
+	 * @return The input fallback mapping for the current XR mode.
+	 */
+	@Keep
+	private fun getInputFallbackMapping(): String? {
+		return xrMode.inputFallbackMapping
+	}
+
+	fun requestPermission(name: String?): Boolean {
+		return requestPermission(name, getActivity())
+	}
+
+	fun requestPermissions(): Boolean {
+		return PermissionsUtil.requestManifestPermissions(getActivity())
+	}
+
+	fun getGrantedPermissions(): Array<String?>? {
+		return PermissionsUtil.getGrantedPermissions(getActivity())
+	}
+
+	@Keep
+	private fun getCACertificates(): String {
+		return GodotNetUtils.getCACertificates()
+	}
+
+	private fun obbIsCorrupted(f: String, mainPackMd5: String): Boolean {
+		return try {
+			val fis: InputStream = FileInputStream(f)
+
+			// Create MD5 Hash
+			val buffer = ByteArray(16384)
+			val complete = MessageDigest.getInstance("MD5")
+			var numRead: Int
+			do {
+				numRead = fis.read(buffer)
+				if (numRead > 0) {
+					complete.update(buffer, 0, numRead)
+				}
+			} while (numRead != -1)
+			fis.close()
+			val messageDigest = complete.digest()
+
+			// Create Hex String
+			val hexString = StringBuilder()
+			for (b in messageDigest) {
+				var s = Integer.toHexString(0xFF and b.toInt())
+				if (s.length == 1) {
+					s = "0$s"
+				}
+				hexString.append(s)
+			}
+			val md5str = hexString.toString()
+			md5str != mainPackMd5
+		} catch (e: java.lang.Exception) {
+			e.printStackTrace()
+			true
+		}
+	}
+
+	@Keep
+	private fun initInputDevices() {
+		renderView!!.initInputDevices()
+	}
+
+	@Keep
+	private fun createNewGodotInstance(args: Array<String>): Int {
+		return primaryHost?.onNewGodotInstanceRequested(args) ?: 0
+	}
+
+	@Keep
+	private fun nativeBeginBenchmarkMeasure(label: String) {
+		beginBenchmarkMeasure(label)
+	}
+
+	@Keep
+	private fun nativeEndBenchmarkMeasure(label: String) {
+		endBenchmarkMeasure(label)
+	}
+
+	@Keep
+	private fun nativeDumpBenchmark(benchmarkFile: String) {
+		dumpBenchmark(fileAccessHandler, benchmarkFile)
+	}
+}

+ 167 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt

@@ -0,0 +1,167 @@
+/**************************************************************************/
+/*  GodotActivity.kt                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.CallSuper
+import androidx.fragment.app.FragmentActivity
+import org.godotengine.godot.utils.ProcessPhoenix
+
+/**
+ * Base abstract activity for Android apps intending to use Godot as the primary screen.
+ *
+ * Also a reference implementation for how to setup and use the [GodotFragment] fragment
+ * within an Android app.
+ */
+abstract class GodotActivity : FragmentActivity(), GodotHost {
+
+	companion object {
+		private val TAG = GodotActivity::class.java.simpleName
+
+		@JvmStatic
+		protected val EXTRA_FORCE_QUIT = "force_quit_requested"
+		@JvmStatic
+		protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
+	}
+
+	/**
+	 * Interaction with the [Godot] object is delegated to the [GodotFragment] class.
+	 */
+	protected var godotFragment: GodotFragment? = null
+		private set
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		super.onCreate(savedInstanceState)
+		setContentView(R.layout.godot_app_layout)
+
+		handleStartIntent(intent, true)
+
+		val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container)
+		if (currentFragment is GodotFragment) {
+			Log.v(TAG, "Reusing existing Godot fragment instance.")
+			godotFragment = currentFragment
+		} else {
+			Log.v(TAG, "Creating new Godot fragment instance.")
+			godotFragment = initGodotInstance()
+			supportFragmentManager.beginTransaction().replace(R.id.godot_fragment_container, godotFragment!!).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss()
+		}
+	}
+
+	override fun onDestroy() {
+		Log.v(TAG, "Destroying Godot app...")
+		super.onDestroy()
+		if (godotFragment != null) {
+			terminateGodotInstance(godotFragment!!.godot)
+		}
+	}
+
+	override fun onGodotForceQuit(instance: Godot) {
+		runOnUiThread { terminateGodotInstance(instance) }
+	}
+
+	private fun terminateGodotInstance(instance: Godot) {
+		if (godotFragment != null && instance === godotFragment!!.godot) {
+			Log.v(TAG, "Force quitting Godot instance")
+			ProcessPhoenix.forceQuit(this)
+		}
+	}
+
+	override fun onGodotRestartRequested(instance: Godot) {
+		runOnUiThread {
+			if (godotFragment != null && instance === godotFragment!!.godot) {
+				// It's very hard to properly de-initialize Godot on Android to restart the game
+				// from scratch. Therefore, we need to kill the whole app process and relaunch it.
+				//
+				// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
+				// releasing and reloading native libs or resetting their state somehow and clearing static data).
+				Log.v(TAG, "Restarting Godot instance...")
+				ProcessPhoenix.triggerRebirth(this)
+			}
+		}
+	}
+
+	override fun onNewIntent(newIntent: Intent) {
+		super.onNewIntent(newIntent)
+		intent = newIntent
+
+		handleStartIntent(newIntent, false)
+
+		godotFragment?.onNewIntent(newIntent)
+	}
+
+	private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
+		val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false)
+		if (forceQuitRequested) {
+			Log.d(TAG, "Force quit requested, terminating..")
+			ProcessPhoenix.forceQuit(this)
+			return
+		}
+		if (!newLaunch) {
+			val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
+			if (newLaunchRequested) {
+				Log.d(TAG, "New launch requested, restarting..")
+				val restartIntent = Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false)
+				ProcessPhoenix.triggerRebirth(this, restartIntent)
+				return
+			}
+		}
+	}
+
+	@CallSuper
+	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+		super.onActivityResult(requestCode, resultCode, data)
+		godotFragment?.onActivityResult(requestCode, resultCode, data)
+	}
+
+	@CallSuper
+	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+		godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
+	}
+
+	override fun onBackPressed() {
+		godotFragment?.onBackPressed() ?: super.onBackPressed()
+	}
+
+	override fun getActivity(): Activity? {
+		return this
+	}
+
+	/**
+	 * Used to initialize the Godot fragment instance in [onCreate].
+	 */
+	protected open fun initGodotInstance(): GodotFragment {
+		return GodotFragment()
+	}
+}

+ 429 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java

@@ -0,0 +1,429 @@
+/**************************************************************************/
+/*  GodotFragment.java                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot;
+
+import org.godotengine.godot.utils.BenchmarkUtils;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Messenger;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
+import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
+import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
+import com.google.android.vending.expansion.downloader.Helpers;
+import com.google.android.vending.expansion.downloader.IDownloaderClient;
+import com.google.android.vending.expansion.downloader.IDownloaderService;
+import com.google.android.vending.expansion.downloader.IStub;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Base fragment for Android apps intending to use Godot for part of the app's UI.
+ */
+public class GodotFragment extends Fragment implements IDownloaderClient, GodotHost {
+	private static final String TAG = GodotFragment.class.getSimpleName();
+
+	private IStub mDownloaderClientStub;
+	private TextView mStatusText;
+	private TextView mProgressFraction;
+	private TextView mProgressPercent;
+	private TextView mAverageSpeed;
+	private TextView mTimeRemaining;
+	private ProgressBar mPB;
+
+	private View mDashboard;
+	private View mCellMessage;
+
+	private Button mPauseButton;
+	private Button mWiFiSettingsButton;
+
+	private FrameLayout godotContainerLayout;
+	private boolean mStatePaused;
+	private int mState;
+
+	@Nullable
+	private GodotHost parentHost;
+	private Godot godot;
+
+	static private Intent mCurrentIntent;
+
+	public void onNewIntent(Intent intent) {
+		mCurrentIntent = intent;
+	}
+
+	static public Intent getCurrentIntent() {
+		return mCurrentIntent;
+	}
+
+	private void setState(int newState) {
+		if (mState != newState) {
+			mState = newState;
+			mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
+		}
+	}
+
+	private void setButtonPausedState(boolean paused) {
+		mStatePaused = paused;
+		int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
+		mPauseButton.setText(stringResourceID);
+	}
+
+	public interface ResultCallback {
+		void callback(int requestCode, int resultCode, Intent data);
+	}
+	public ResultCallback resultCallback;
+
+	public Godot getGodot() {
+		return godot;
+	}
+
+	@Override
+	public void onAttach(@NonNull Context context) {
+		super.onAttach(context);
+		if (getParentFragment() instanceof GodotHost) {
+			parentHost = (GodotHost)getParentFragment();
+		} else if (getActivity() instanceof GodotHost) {
+			parentHost = (GodotHost)getActivity();
+		}
+	}
+
+	@Override
+	public void onDetach() {
+		super.onDetach();
+		parentHost = null;
+	}
+
+	@CallSuper
+	@Override
+	public void onActivityResult(int requestCode, int resultCode, Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		if (resultCallback != null) {
+			resultCallback.callback(requestCode, resultCode, data);
+			resultCallback = null;
+		}
+
+		godot.onActivityResult(requestCode, resultCode, data);
+	}
+
+	@CallSuper
+	@Override
+	public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+		godot.onRequestPermissionsResult(requestCode, permissions, grantResults);
+	}
+
+	@Override
+	public void onServiceConnected(Messenger m) {
+		IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m);
+		remoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
+	}
+
+	@Override
+	public void onCreate(Bundle icicle) {
+		BenchmarkUtils.beginBenchmarkMeasure("GodotFragment::onCreate");
+		super.onCreate(icicle);
+
+		final Activity activity = getActivity();
+		mCurrentIntent = activity.getIntent();
+
+		godot = new Godot(requireContext());
+		performEngineInitialization();
+		BenchmarkUtils.endBenchmarkMeasure("GodotFragment::onCreate");
+	}
+
+	private void performEngineInitialization() {
+		try {
+			godot.onCreate(this);
+
+			if (!godot.onInitNativeLayer(this)) {
+				throw new IllegalStateException("Unable to initialize engine native layer");
+			}
+
+			godotContainerLayout = godot.onInitRenderView(this);
+			if (godotContainerLayout == null) {
+				throw new IllegalStateException("Unable to initialize engine render view");
+			}
+		} catch (IllegalArgumentException ignored) {
+			final Activity activity = getActivity();
+			Intent notifierIntent = new Intent(activity, activity.getClass());
+			notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+			PendingIntent pendingIntent;
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+				pendingIntent = PendingIntent.getActivity(activity, 0,
+						notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+			} else {
+				pendingIntent = PendingIntent.getActivity(activity, 0,
+						notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+			}
+
+			int startResult;
+			try {
+				startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(getContext(), pendingIntent, GodotDownloaderService.class);
+
+				if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
+					// This is where you do set up to display the download
+					// progress (next step in onCreateView)
+					mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, GodotDownloaderService.class);
+					return;
+				}
+
+				// Restart engine initialization
+				performEngineInitialization();
+			} catch (PackageManager.NameNotFoundException e) {
+				Log.e(TAG, "Unable to start download service", e);
+			}
+		}
+	}
+
+	@Override
+	public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle icicle) {
+		if (mDownloaderClientStub != null) {
+			View downloadingExpansionView =
+					inflater.inflate(R.layout.downloading_expansion, container, false);
+			mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar);
+			mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText);
+			mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction);
+			mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage);
+			mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed);
+			mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining);
+			mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
+			mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
+			mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
+			mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton);
+
+			return downloadingExpansionView;
+		}
+
+		return godotContainerLayout;
+	}
+
+	@Override
+	public void onDestroy() {
+		godot.onDestroy(this);
+		super.onDestroy();
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+
+		if (!godot.isInitialized()) {
+			if (null != mDownloaderClientStub) {
+				mDownloaderClientStub.disconnect(getActivity());
+			}
+			return;
+		}
+
+		godot.onPause(this);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		if (!godot.isInitialized()) {
+			if (null != mDownloaderClientStub) {
+				mDownloaderClientStub.connect(getActivity());
+			}
+			return;
+		}
+
+		godot.onResume(this);
+	}
+
+	public void onBackPressed() {
+		godot.onBackPressed(this);
+	}
+
+	/**
+	 * The download state should trigger changes in the UI --- it may be useful
+	 * to show the state as being indeterminate at times. This sample can be
+	 * considered a guideline.
+	 */
+	@Override
+	public void onDownloadStateChanged(int newState) {
+		setState(newState);
+		boolean showDashboard = true;
+		boolean showCellMessage = false;
+		boolean paused;
+		boolean indeterminate;
+		switch (newState) {
+			case IDownloaderClient.STATE_IDLE:
+				// STATE_IDLE means the service is listening, so it's
+				// safe to start making remote service calls.
+				paused = false;
+				indeterminate = true;
+				break;
+			case IDownloaderClient.STATE_CONNECTING:
+			case IDownloaderClient.STATE_FETCHING_URL:
+				showDashboard = true;
+				paused = false;
+				indeterminate = true;
+				break;
+			case IDownloaderClient.STATE_DOWNLOADING:
+				paused = false;
+				showDashboard = true;
+				indeterminate = false;
+				break;
+
+			case IDownloaderClient.STATE_FAILED_CANCELED:
+			case IDownloaderClient.STATE_FAILED:
+			case IDownloaderClient.STATE_FAILED_FETCHING_URL:
+			case IDownloaderClient.STATE_FAILED_UNLICENSED:
+				paused = true;
+				showDashboard = false;
+				indeterminate = false;
+				break;
+			case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
+			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
+				showDashboard = false;
+				paused = true;
+				indeterminate = false;
+				showCellMessage = true;
+				break;
+
+			case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
+				paused = true;
+				indeterminate = false;
+				break;
+			case IDownloaderClient.STATE_PAUSED_ROAMING:
+			case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
+				paused = true;
+				indeterminate = false;
+				break;
+			case IDownloaderClient.STATE_COMPLETED:
+				showDashboard = false;
+				paused = false;
+				indeterminate = false;
+				performEngineInitialization();
+				return;
+			default:
+				paused = true;
+				indeterminate = true;
+				showDashboard = true;
+		}
+		int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
+		if (mDashboard.getVisibility() != newDashboardVisibility) {
+			mDashboard.setVisibility(newDashboardVisibility);
+		}
+		int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
+		if (mCellMessage.getVisibility() != cellMessageVisibility) {
+			mCellMessage.setVisibility(cellMessageVisibility);
+		}
+
+		mPB.setIndeterminate(indeterminate);
+		setButtonPausedState(paused);
+	}
+
+	@Override
+	public void onDownloadProgress(DownloadProgressInfo progress) {
+		mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
+				Helpers.getSpeedString(progress.mCurrentSpeed)));
+		mTimeRemaining.setText(getString(R.string.time_remaining,
+				Helpers.getTimeRemaining(progress.mTimeRemaining)));
+
+		mPB.setMax((int)(progress.mOverallTotal >> 8));
+		mPB.setProgress((int)(progress.mOverallProgress >> 8));
+		mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal));
+		mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
+				progress.mOverallTotal));
+	}
+
+	@CallSuper
+	@Override
+	public List<String> getCommandLine() {
+		return parentHost != null ? parentHost.getCommandLine() : Collections.emptyList();
+	}
+
+	@CallSuper
+	@Override
+	public void onGodotSetupCompleted() {
+		if (parentHost != null) {
+			parentHost.onGodotSetupCompleted();
+		}
+	}
+
+	@CallSuper
+	@Override
+	public void onGodotMainLoopStarted() {
+		if (parentHost != null) {
+			parentHost.onGodotMainLoopStarted();
+		}
+	}
+
+	@Override
+	public void onGodotForceQuit(Godot instance) {
+		if (parentHost != null) {
+			parentHost.onGodotForceQuit(instance);
+		}
+	}
+
+	@Override
+	public boolean onGodotForceQuit(int godotInstanceId) {
+		return parentHost != null && parentHost.onGodotForceQuit(godotInstanceId);
+	}
+
+	@Override
+	public void onGodotRestartRequested(Godot instance) {
+		if (parentHost != null) {
+			parentHost.onGodotRestartRequested(instance);
+		}
+	}
+
+	@Override
+	public int onNewGodotInstanceRequested(String[] args) {
+		if (parentHost != null) {
+			return parentHost.onNewGodotInstanceRequested(args);
+		}
+		return 0;
+	}
+}

+ 12 - 8
platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java

@@ -29,10 +29,10 @@
 /**************************************************************************/
 
 package org.godotengine.godot;
+
 import org.godotengine.godot.gl.GLSurfaceView;
 import org.godotengine.godot.gl.GodotRenderer;
 import org.godotengine.godot.input.GodotInputHandler;
-import org.godotengine.godot.utils.GLUtils;
 import org.godotengine.godot.xr.XRMode;
 import org.godotengine.godot.xr.ovr.OvrConfigChooser;
 import org.godotengine.godot.xr.ovr.OvrContextFactory;
@@ -78,22 +78,23 @@ import java.io.InputStream;
  *   bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
  */
 public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
+	private final GodotHost host;
 	private final Godot godot;
 	private final GodotInputHandler inputHandler;
 	private final GodotRenderer godotRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 
-	public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) {
-		super(context);
-		GLUtils.use_debug_opengl = p_use_debug_opengl;
+	public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) {
+		super(host.getActivity());
 
+		this.host = host;
 		this.godot = godot;
 		this.inputHandler = new GodotInputHandler(this);
 		this.godotRenderer = new GodotRenderer();
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
 		}
-		init(xrMode, false);
+		init(xrMode, false, useDebugOpengl);
 	}
 
 	@Override
@@ -123,7 +124,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
 
 	@Override
 	public void onBackPressed() {
-		godot.onBackPressed();
+		godot.onBackPressed(host);
 	}
 
 	@Override
@@ -233,7 +234,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
 		return super.onResolvePointerIcon(me, pointerIndex);
 	}
 
-	private void init(XRMode xrMode, boolean translucent) {
+	private void init(XRMode xrMode, boolean translucent, boolean useDebugOpengl) {
 		setPreserveEGLContextOnPause(true);
 		setFocusableInTouchMode(true);
 		switch (xrMode) {
@@ -262,7 +263,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
 				/* Setup the context factory for 2.0 rendering.
 				 * See ContextFactory class definition below
 				 */
-				setEGLContextFactory(new RegularContextFactory());
+				setEGLContextFactory(new RegularContextFactory(useDebugOpengl));
 
 				/* We need to choose an EGLConfig that matches the format of
 				 * our surface exactly. This is going to be done in our
@@ -275,7 +276,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
 								new RegularConfigChooser(8, 8, 8, 8, 16, 0)));
 				break;
 		}
+	}
 
+	@Override
+	public void startRenderer() {
 		/* Set the renderer responsible for frame rendering */
 		setRenderer(godotRenderer);
 	}

+ 8 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -30,11 +30,13 @@
 
 package org.godotengine.godot;
 
+import android.app.Activity;
+
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment.
+ * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} engine.
  */
 public interface GodotHost {
 	/**
@@ -86,4 +88,9 @@ public interface GodotHost {
 	default int onNewGodotInstanceRequested(String[] args) {
 		return 0;
 	}
+
+	/**
+	 * Provide access to the Activity hosting the Godot engine.
+	 */
+	Activity getActivity();
 }

+ 5 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java

@@ -39,6 +39,11 @@ public interface GodotRenderView {
 
 	void initInputDevices();
 
+	/**
+	 * Starts the thread that will drive Godot's rendering.
+	 */
+	void startRenderer();
+
 	void queueOnRenderThread(Runnable event);
 
 	void onActivityPaused();

+ 54 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotService.kt

@@ -0,0 +1,54 @@
+package org.godotengine.godot
+
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import android.util.Log
+
+/**
+ * Godot service responsible for hosting the Godot engine instance.
+ */
+class GodotService : Service() {
+
+	companion object {
+		private val TAG = GodotService::class.java.simpleName
+	}
+
+	private var boundIntent: Intent? = null
+	private val godot by lazy {
+		Godot(applicationContext)
+	}
+
+	override fun onCreate() {
+		super.onCreate()
+	}
+
+	override fun onDestroy() {
+		super.onDestroy()
+	}
+
+	override fun onBind(intent: Intent?): IBinder? {
+		if (boundIntent != null) {
+			Log.d(TAG, "GodotService already bound")
+			return null
+		}
+
+		boundIntent = intent
+		return GodotHandle(godot)
+	}
+
+	override fun onRebind(intent: Intent?) {
+		super.onRebind(intent)
+	}
+
+	override fun onUnbind(intent: Intent?): Boolean {
+		return super.onUnbind(intent)
+	}
+
+	override fun onTaskRemoved(rootIntent: Intent?) {
+		super.onTaskRemoved(rootIntent)
+	}
+
+	class GodotHandle(val godot: Godot) : Binder()
+}

+ 9 - 4
platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java

@@ -35,7 +35,6 @@ import org.godotengine.godot.vulkan.VkRenderer;
 import org.godotengine.godot.vulkan.VkSurfaceView;
 
 import android.annotation.SuppressLint;
-import android.content.Context;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -52,14 +51,16 @@ import androidx.annotation.Keep;
 import java.io.InputStream;
 
 public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
+	private final GodotHost host;
 	private final Godot godot;
 	private final GodotInputHandler mInputHandler;
 	private final VkRenderer mRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 
-	public GodotVulkanRenderView(Context context, Godot godot) {
-		super(context);
+	public GodotVulkanRenderView(GodotHost host, Godot godot) {
+		super(host.getActivity());
 
+		this.host = host;
 		this.godot = godot;
 		mInputHandler = new GodotInputHandler(this);
 		mRenderer = new VkRenderer();
@@ -67,6 +68,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
 		}
 		setFocusableInTouchMode(true);
+	}
+
+	@Override
+	public void startRenderer() {
 		startRenderer(mRenderer);
 	}
 
@@ -97,7 +102,7 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
 
 	@Override
 	public void onBackPressed() {
-		godot.onBackPressed();
+		godot.onBackPressed(host);
 	}
 
 	@Override

+ 5 - 4
platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java

@@ -33,6 +33,7 @@ package org.godotengine.godot.tts;
 import org.godotengine.godot.GodotLib;
 
 import android.app.Activity;
+import android.content.Context;
 import android.os.Bundle;
 import android.speech.tts.TextToSpeech;
 import android.speech.tts.UtteranceProgressListener;
@@ -62,7 +63,7 @@ public class GodotTTS extends UtteranceProgressListener {
 	final private static int EVENT_CANCEL = 2;
 	final private static int EVENT_BOUNDARY = 3;
 
-	final private Activity activity;
+	private final Context context;
 	private TextToSpeech synth;
 	private LinkedList<GodotUtterance> queue;
 	final private Object lock = new Object();
@@ -71,8 +72,8 @@ public class GodotTTS extends UtteranceProgressListener {
 	private boolean speaking;
 	private boolean paused;
 
-	public GodotTTS(Activity p_activity) {
-		activity = p_activity;
+	public GodotTTS(Context context) {
+		this.context = context;
 	}
 
 	private void updateTTS() {
@@ -188,7 +189,7 @@ public class GodotTTS extends UtteranceProgressListener {
 	 * Initialize synth and query.
 	 */
 	public void init() {
-		synth = new TextToSpeech(activity, null);
+		synth = new TextToSpeech(context, null);
 		queue = new LinkedList<GodotUtterance>();
 
 		synth.setOnUtteranceProgressListener(this);

+ 0 - 2
platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java

@@ -44,8 +44,6 @@ public class GLUtils {
 
 	public static final boolean DEBUG = false;
 
-	public static boolean use_debug_opengl = false;
-
 	private static final String[] ATTRIBUTES_NAMES = new String[] {
 		"EGL_BUFFER_SIZE",
 		"EGL_ALPHA_SIZE",

+ 6 - 5
platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java

@@ -36,7 +36,8 @@ import android.net.wifi.WifiManager;
 import android.util.Base64;
 import android.util.Log;
 
-import java.io.StringWriter;
+import androidx.annotation.NonNull;
+
 import java.security.KeyStore;
 import java.security.cert.X509Certificate;
 import java.util.Enumeration;
@@ -50,9 +51,9 @@ public class GodotNetUtils {
 	/* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */
 	private WifiManager.MulticastLock multicastLock;
 
-	public GodotNetUtils(Activity p_activity) {
-		if (PermissionsUtil.hasManifestPermission(p_activity, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
-			WifiManager wifi = (WifiManager)p_activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+	public GodotNetUtils(Context context) {
+		if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
+			WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
 			multicastLock = wifi.createMulticastLock("GodotMulticastLock");
 			multicastLock.setReferenceCounted(true);
 		}
@@ -91,7 +92,7 @@ public class GodotNetUtils {
 	 * @see https://developer.android.com/reference/java/security/KeyStore .
 	 * @return A string of concatenated X509 certificates in PEM format.
 	 */
-	public static String getCACertificates() {
+	public static @NonNull String getCACertificates() {
 		try {
 			KeyStore ks = KeyStore.getInstance("AndroidCAStore");
 			StringBuilder writer = new StringBuilder();

+ 16 - 16
platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java

@@ -32,6 +32,7 @@ package org.godotengine.godot.utils;
 
 import android.Manifest;
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -52,7 +53,6 @@ import java.util.Set;
 /**
  * This class includes utility functions for Android permissions related operations.
  */
-
 public final class PermissionsUtil {
 	private static final String TAG = PermissionsUtil.class.getSimpleName();
 
@@ -193,13 +193,13 @@ public final class PermissionsUtil {
 
 	/**
 	 * With this function you can get the list of dangerous permissions that have been granted to the Android application.
-	 * @param activity the caller activity for this method.
+	 * @param context the caller context for this method.
 	 * @return granted permissions list
 	 */
-	public static String[] getGrantedPermissions(Activity activity) {
+	public static String[] getGrantedPermissions(Context context) {
 		String[] manifestPermissions;
 		try {
-			manifestPermissions = getManifestPermissions(activity);
+			manifestPermissions = getManifestPermissions(context);
 		} catch (PackageManager.NameNotFoundException e) {
 			e.printStackTrace();
 			return new String[0];
@@ -215,9 +215,9 @@ public final class PermissionsUtil {
 						grantedPermissions.add(manifestPermission);
 					}
 				} else {
-					PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
+					PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission);
 					int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
-					if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
+					if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
 						grantedPermissions.add(manifestPermission);
 					}
 				}
@@ -232,13 +232,13 @@ public final class PermissionsUtil {
 
 	/**
 	 * Check if the given permission is in the AndroidManifest.xml file.
-	 * @param activity the caller activity for this method.
+	 * @param context the caller context for this method.
 	 * @param permission the permession to look for in the manifest file.
 	 * @return "true" if the permission is in the manifest file of the activity, "false" otherwise.
 	 */
-	public static boolean hasManifestPermission(Activity activity, String permission) {
+	public static boolean hasManifestPermission(Context context, String permission) {
 		try {
-			for (String p : getManifestPermissions(activity)) {
+			for (String p : getManifestPermissions(context)) {
 				if (permission.equals(p))
 					return true;
 			}
@@ -250,13 +250,13 @@ public final class PermissionsUtil {
 
 	/**
 	 * Returns the permissions defined in the AndroidManifest.xml file.
-	 * @param activity the caller activity for this method.
+	 * @param context the caller context for this method.
 	 * @return manifest permissions list
 	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
 	 */
-	private static String[] getManifestPermissions(Activity activity) throws PackageManager.NameNotFoundException {
-		PackageManager packageManager = activity.getPackageManager();
-		PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS);
+	private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
+		PackageManager packageManager = context.getPackageManager();
+		PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
 		if (packageInfo.requestedPermissions == null)
 			return new String[0];
 		return packageInfo.requestedPermissions;
@@ -264,13 +264,13 @@ public final class PermissionsUtil {
 
 	/**
 	 * Returns the information of the desired permission.
-	 * @param activity the caller activity for this method.
+	 * @param context the caller context for this method.
 	 * @param permission the name of the permission.
 	 * @return permission info object
 	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
 	 */
-	private static PermissionInfo getPermissionInfo(Activity activity, String permission) throws PackageManager.NameNotFoundException {
-		PackageManager packageManager = activity.getPackageManager();
+	private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException {
+		PackageManager packageManager = context.getPackageManager();
 		return packageManager.getPermissionInfo(permission, 0);
 	}
 }

+ 11 - 1
platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java

@@ -51,12 +51,22 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {
 
 	private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
 
+	private final boolean mUseDebugOpengl;
+
+	public RegularContextFactory() {
+		this(false);
+	}
+
+	public RegularContextFactory(boolean useDebugOpengl) {
+		this.mUseDebugOpengl = useDebugOpengl;
+	}
+
 	public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
 		Log.w(TAG, "creating OpenGL ES 3.0 context :");
 
 		GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);
 		EGLContext context;
-		if (GLUtils.use_debug_opengl) {
+		if (mUseDebugOpengl) {
 			int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
 			context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
 		} else {

+ 1 - 1
platform/android/java_godot_lib_jni.cpp

@@ -135,7 +135,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv
 
 	os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion);
 
-	return godot_java->on_video_init(env);
+	return true;
 }
 
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {

+ 3 - 73
platform/android/java_godot_wrapper.cpp

@@ -58,12 +58,10 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	}
 
 	// get some Godot method pointers...
-	_on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()Z");
 	_restart = p_env->GetMethodID(godot_class, "restart", "()V");
 	_finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z");
 	_set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V");
 	_alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V");
-	_get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I");
 	_get_clipboard = p_env->GetMethodID(godot_class, "getClipboard", "()Ljava/lang/String;");
 	_set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V");
 	_has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z");
@@ -72,20 +70,15 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;");
 	_get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;");
 	_init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V");
-	_get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;");
-	_is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z");
 	_vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V");
 	_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
 	_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
 	_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
 	_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
 	_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
-	_begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V");
-	_end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V");
-	_dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V");
-
-	// get some Activity method pointers...
-	_get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
+	_begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V");
+	_end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V");
+	_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
 }
 
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -105,29 +98,6 @@ jobject GodotJavaWrapper::get_activity() {
 	return activity;
 }
 
-jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env) {
-	if (godot_class) {
-		if (p_env == nullptr) {
-			p_env = get_jni_env();
-		}
-		ERR_FAIL_NULL_V(p_env, nullptr);
-		jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class);
-		return p_env->GetStaticObjectField(godot_class, fid);
-	} else {
-		return nullptr;
-	}
-}
-
-jobject GodotJavaWrapper::get_class_loader() {
-	if (_get_class_loader) {
-		JNIEnv *env = get_jni_env();
-		ERR_FAIL_NULL_V(env, nullptr);
-		return env->CallObjectMethod(activity, _get_class_loader);
-	} else {
-		return nullptr;
-	}
-}
-
 GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
 	if (godot_view != nullptr) {
 		return godot_view;
@@ -143,17 +113,6 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
 	return godot_view;
 }
 
-bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
-	if (_on_video_init) {
-		if (p_env == nullptr) {
-			p_env = get_jni_env();
-		}
-		ERR_FAIL_NULL_V(p_env, false);
-		return p_env->CallBooleanMethod(godot_instance, _on_video_init);
-	}
-	return false;
-}
-
 void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) {
 	if (_on_godot_setup_completed) {
 		if (p_env == nullptr) {
@@ -212,15 +171,6 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
 	}
 }
 
-int GodotJavaWrapper::get_gles_version_code() {
-	JNIEnv *env = get_jni_env();
-	ERR_FAIL_NULL_V(env, 0);
-	if (_get_GLES_version_code) {
-		return env->CallIntMethod(godot_instance, _get_GLES_version_code);
-	}
-	return 0;
-}
-
 bool GodotJavaWrapper::has_get_clipboard() {
 	return _get_clipboard != nullptr;
 }
@@ -333,26 +283,6 @@ void GodotJavaWrapper::init_input_devices() {
 	}
 }
 
-jobject GodotJavaWrapper::get_surface() {
-	if (_get_surface) {
-		JNIEnv *env = get_jni_env();
-		ERR_FAIL_NULL_V(env, nullptr);
-		return env->CallObjectMethod(godot_instance, _get_surface);
-	} else {
-		return nullptr;
-	}
-}
-
-bool GodotJavaWrapper::is_activity_resumed() {
-	if (_is_activity_resumed) {
-		JNIEnv *env = get_jni_env();
-		ERR_FAIL_NULL_V(env, false);
-		return env->CallBooleanMethod(godot_instance, _is_activity_resumed);
-	} else {
-		return false;
-	}
-}
-
 void GodotJavaWrapper::vibrate(int p_duration_ms) {
 	if (_vibrate) {
 		JNIEnv *env = get_jni_env();

+ 0 - 11
platform/android/java_godot_wrapper.h

@@ -49,12 +49,10 @@ private:
 
 	GodotJavaViewWrapper *godot_view = nullptr;
 
-	jmethodID _on_video_init = nullptr;
 	jmethodID _restart = nullptr;
 	jmethodID _finish = nullptr;
 	jmethodID _set_keep_screen_on = nullptr;
 	jmethodID _alert = nullptr;
-	jmethodID _get_GLES_version_code = nullptr;
 	jmethodID _get_clipboard = nullptr;
 	jmethodID _set_clipboard = nullptr;
 	jmethodID _has_clipboard = nullptr;
@@ -63,13 +61,10 @@ private:
 	jmethodID _get_granted_permissions = nullptr;
 	jmethodID _get_ca_certificates = nullptr;
 	jmethodID _init_input_devices = nullptr;
-	jmethodID _get_surface = nullptr;
-	jmethodID _is_activity_resumed = nullptr;
 	jmethodID _vibrate = nullptr;
 	jmethodID _get_input_fallback_mapping = nullptr;
 	jmethodID _on_godot_setup_completed = nullptr;
 	jmethodID _on_godot_main_loop_started = nullptr;
-	jmethodID _get_class_loader = nullptr;
 	jmethodID _create_new_godot_instance = nullptr;
 	jmethodID _get_render_view = nullptr;
 	jmethodID _begin_benchmark_measure = nullptr;
@@ -81,19 +76,15 @@ public:
 	~GodotJavaWrapper();
 
 	jobject get_activity();
-	jobject get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env = nullptr);
 
-	jobject get_class_loader();
 	GodotJavaViewWrapper *get_godot_view();
 
-	bool on_video_init(JNIEnv *p_env = nullptr);
 	void on_godot_setup_completed(JNIEnv *p_env = nullptr);
 	void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
 	void restart(JNIEnv *p_env = nullptr);
 	bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0);
 	void set_keep_screen_on(bool p_enabled);
 	void alert(const String &p_message, const String &p_title);
-	int get_gles_version_code();
 	bool has_get_clipboard();
 	String get_clipboard();
 	bool has_set_clipboard();
@@ -105,8 +96,6 @@ public:
 	Vector<String> get_granted_permissions() const;
 	String get_ca_certificates() const;
 	void init_input_devices();
-	jobject get_surface();
-	bool is_activity_resumed();
 	void vibrate(int p_duration_ms);
 	String get_input_fallback_mapping();
 	int create_new_godot_instance(List<String> args);