2
0
Эх сурвалжийг харах

Provide a delegate implementation for the `killProcess` logic on Android

The implementation forwards the kill request to the Godot host for handling. If the Godot host is unable to handle the request, it falls back to the `OS_Unix::kill(...)` implementation.
Fredia Huya-Kouadio 2 жил өмнө
parent
commit
48a55ffad3

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

@@ -31,12 +31,11 @@
 package org.godotengine.editor
 
 import android.Manifest
+import android.app.ActivityManager
+import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Bundle
-import android.os.Debug
-import android.os.Environment
+import android.os.*
 import android.util.Log
 import android.widget.Toast
 import androidx.window.layout.WindowMetricsCalculator
@@ -64,11 +63,18 @@ open class GodotEditor : FullScreenGodotApp() {
 
 		private const val COMMAND_LINE_PARAMS = "command_line_params"
 
+		private const val EDITOR_ID = 777
 		private const val EDITOR_ARG = "--editor"
 		private const val EDITOR_ARG_SHORT = "-e"
+		private const val EDITOR_PROCESS_NAME_SUFFIX = ":GodotEditor"
 
+		private const val GAME_ID = 667
+		private const val GAME_PROCESS_NAME_SUFFIX = ":GodotGame"
+
+		private const val PROJECT_MANAGER_ID = 555
 		private const val PROJECT_MANAGER_ARG = "--project-manager"
 		private const val PROJECT_MANAGER_ARG_SHORT = "-p"
+		private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager"
 	}
 
 	private val commandLineParams = ArrayList<String>()
@@ -102,9 +108,10 @@ open class GodotEditor : FullScreenGodotApp() {
 
 	override fun getCommandLine() = commandLineParams
 
-	override fun onNewGodotInstanceRequested(args: Array<String>) {
+	override fun onNewGodotInstanceRequested(args: Array<String>): Int {
 		// Parse the arguments to figure out which activity to start.
 		var targetClass: Class<*> = GodotGame::class.java
+		var instanceId = GAME_ID
 
 		// Whether we should launch the new godot instance in an adjacent window
 		// https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT
@@ -115,12 +122,14 @@ open class GodotEditor : FullScreenGodotApp() {
 			if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) {
 				targetClass = GodotEditor::class.java
 				launchAdjacent = false
+				instanceId = EDITOR_ID
 				break
 			}
 
 			if (PROJECT_MANAGER_ARG == arg || PROJECT_MANAGER_ARG_SHORT == arg) {
 				targetClass = GodotProjectManager::class.java
 				launchAdjacent = false
+				instanceId = PROJECT_MANAGER_ID
 				break
 			}
 		}
@@ -139,6 +148,37 @@ open class GodotEditor : FullScreenGodotApp() {
 			Log.d(TAG, "Starting $targetClass")
 			startActivity(newInstance)
 		}
+		return instanceId
+	}
+
+	override fun onGodotForceQuit(godotInstanceId: Int): Boolean {
+		val processNameSuffix = when (godotInstanceId) {
+			GAME_ID -> {
+				GAME_PROCESS_NAME_SUFFIX
+			}
+			EDITOR_ID -> {
+				EDITOR_PROCESS_NAME_SUFFIX
+			}
+			PROJECT_MANAGER_ID -> {
+				PROJECT_MANAGER_PROCESS_NAME_SUFFIX
+			}
+			else -> ""
+		}
+		if (processNameSuffix.isBlank()) {
+			return false
+		}
+
+		val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+		val runningProcesses = activityManager.runningAppProcesses
+		for (runningProcess in runningProcesses) {
+			if (runningProcess.processName.endsWith(processNameSuffix)) {
+				Log.v(TAG, "Killing Godot process ${runningProcess.processName}")
+				Process.killProcess(runningProcess.pid)
+				return true
+			}
+		}
+
+		return false
 	}
 
 	// Get the screen's density scale

+ 19 - 11
platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java

@@ -74,28 +74,36 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
 	public void onDestroy() {
 		Log.v(TAG, "Destroying Godot app...");
 		super.onDestroy();
-		onGodotForceQuit(godotFragment);
+		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(this);
+			ProcessPhoenix.forceQuit(FullScreenGodotApp.this);
 		}
 	}
 
 	@Override
 	public final void onGodotRestartRequested(Godot instance) {
-		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 statics).
-			Log.v(TAG, "Restarting Godot instance...");
-			ProcessPhoenix.triggerRebirth(this);
-		}
+		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 statics).
+				Log.v(TAG, "Restarting Godot instance...");
+				ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this);
+			}
+		});
 	}
 
 	@Override

+ 22 - 16
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -462,11 +462,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	}
 
 	public void restart() {
-		runOnUiThread(() -> {
-			if (godotHost != null) {
-				godotHost.onGodotRestartRequested(this);
-			}
-		});
+		if (godotHost != null) {
+			godotHost.onGodotRestartRequested(this);
+		}
 	}
 
 	public void alert(final String message, final String title) {
@@ -1022,11 +1020,20 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	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.
-		runOnUiThread(() -> {
-			if (godotHost != null) {
-				godotHost.onGodotForceQuit(this);
-			}
-		});
+		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) {
@@ -1180,11 +1187,10 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	}
 
 	@Keep
-	private void createNewGodotInstance(String[] args) {
-		runOnUiThread(() -> {
-			if (godotHost != null) {
-				godotHost.onNewGodotInstanceRequested(args);
-			}
-		});
+	private int createNewGodotInstance(String[] args) {
+		if (godotHost != null) {
+			return godotHost.onNewGodotInstanceRequested(args);
+		}
+		return 0;
 	}
 }

+ 18 - 4
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -55,21 +55,35 @@ public interface GodotHost {
 	default void onGodotMainLoopStarted() {}
 
 	/**
-	 * Invoked on the UI thread as the last step of the Godot instance clean up phase.
+	 * Invoked on the render thread to terminate the given Godot instance.
 	 */
 	default void onGodotForceQuit(Godot instance) {}
 
 	/**
-	 * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host
+	 * Invoked on the render thread to terminate the Godot instance with the given id.
+	 * @param godotInstanceId id of the Godot instance to terminate. See {@code onNewGodotInstanceRequested}
+	 *
+	 * @return true if successful, false otherwise.
+	 */
+	default boolean onGodotForceQuit(int godotInstanceId) {
+		return false;
+	}
+
+	/**
+	 * Invoked on the render thread when the Godot instance wants to be restarted. It's up to the host
 	 * to perform the appropriate action(s).
 	 */
 	default void onGodotRestartRequested(Godot instance) {}
 
 	/**
-	 * Invoked on the UI thread when a new Godot instance is requested. It's up to the host to
+	 * Invoked on the render thread when a new Godot instance is requested. It's up to the host to
 	 * perform the appropriate action(s).
 	 *
 	 * @param args Arguments used to initialize the new instance.
+	 *
+	 * @return the id of the new instance. See {@code onGodotForceQuit}
 	 */
-	default void onNewGodotInstanceRequested(String[] args) {}
+	default int onNewGodotInstanceRequested(String[] args) {
+		return 0;
+	}
 }

+ 11 - 8
platform/android/java_godot_wrapper.cpp

@@ -63,7 +63,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_destroy_offscreen_gl = p_env->GetMethodID(godot_class, "destroyOffscreenGL", "()V");
 	_set_offscreen_gl_current = p_env->GetMethodID(godot_class, "setOffscreenGLCurrent", "(Z)V");
 	_restart = p_env->GetMethodID(godot_class, "restart", "()V");
-	_finish = p_env->GetMethodID(godot_class, "forceQuit", "()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");
@@ -80,7 +80,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_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;)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/GodotView;");
 
 	// get some Activity method pointers...
@@ -206,14 +206,15 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) {
 	}
 }
 
-void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
+bool GodotJavaWrapper::force_quit(JNIEnv *p_env, int p_instance_id) {
 	if (_finish) {
 		if (p_env == nullptr) {
 			p_env = get_jni_env();
 		}
-		ERR_FAIL_NULL(p_env);
-		p_env->CallVoidMethod(godot_instance, _finish);
+		ERR_FAIL_NULL_V(p_env, false);
+		return p_env->CallBooleanMethod(godot_instance, _finish, p_instance_id);
 	}
+	return false;
 }
 
 void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {
@@ -372,14 +373,16 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) {
 	}
 }
 
-void GodotJavaWrapper::create_new_godot_instance(List<String> args) {
+int GodotJavaWrapper::create_new_godot_instance(List<String> args) {
 	if (_create_new_godot_instance) {
 		JNIEnv *env = get_jni_env();
-		ERR_FAIL_NULL(env);
+		ERR_FAIL_NULL_V(env, 0);
 		jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
 		for (int i = 0; i < args.size(); i++) {
 			env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
 		}
-		env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs);
+		return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs);
+	} else {
+		return 0;
 	}
 }

+ 2 - 2
platform/android/java_godot_wrapper.h

@@ -94,7 +94,7 @@ public:
 	void on_godot_setup_completed(JNIEnv *p_env = NULL);
 	void on_godot_main_loop_started(JNIEnv *p_env = NULL);
 	void restart(JNIEnv *p_env = NULL);
-	void force_quit(JNIEnv *p_env = NULL);
+	bool force_quit(JNIEnv *p_env = NULL, 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();
@@ -112,7 +112,7 @@ public:
 	bool is_activity_resumed();
 	void vibrate(int p_duration_ms);
 	String get_input_fallback_mapping();
-	void create_new_godot_instance(List<String> args);
+	int create_new_godot_instance(List<String> args);
 };
 
 #endif // JAVA_GODOT_WRAPPER_H

+ 11 - 1
platform/android/os_android.cpp

@@ -716,9 +716,19 @@ Error OS_Android::execute(const String &p_path, const List<String> &p_arguments,
 }
 
 Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) {
-	godot_java->create_new_godot_instance(p_arguments);
+	int instance_id = godot_java->create_new_godot_instance(p_arguments);
+	if (r_child_id) {
+		*r_child_id = instance_id;
+	}
 	return OK;
 }
 
+Error OS_Android::kill(const ProcessID &p_pid) {
+	if (godot_java->force_quit(nullptr, p_pid)) {
+		return OK;
+	}
+	return OS_Unix::kill(p_pid);
+}
+
 OS_Android::~OS_Android() {
 }

+ 1 - 0
platform/android/os_android.h

@@ -215,6 +215,7 @@ public:
 	virtual String get_config_path() const;
 
 	virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false);
+	virtual Error kill(const ProcessID &p_pid);
 
 	virtual bool _check_internal_feature_support(const String &p_feature);
 	OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);