From b162e7ac39c792a3137f3b79de2f644d61dcc9b5 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Mon, 23 May 2022 12:33:15 -0700 Subject: [PATCH] Fix the logic to restart the Godot application --- COPYRIGHT.txt | 5 + misc/scripts/clang_format.sh | 2 + platform/android/export/export_plugin.cpp | 5 - .../android/export/gradle_export_util.cpp | 13 -- platform/android/export/gradle_export_util.h | 2 - platform/android/java/lib/AndroidManifest.xml | 13 +- .../godotengine/godot/FullScreenGodotApp.java | 20 +-- .../godot/GodotInstrumentation.java | 50 ------- .../godot/utils/ProcessPhoenix.java | 141 ++++++++++++++++++ 9 files changed, 163 insertions(+), 88 deletions(-) delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java create mode 100644 platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 52cdbd321f8..64d9a93b9f2 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -86,6 +86,11 @@ Comment: The Android Open Source Project Copyright: 2002, Google Inc. License: Apache-2.0 +Files: ./platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +Comment: ProcessPhoenix +Copyright: 2015, Jake Wharton +License: Apache-2.0 + Files: ./platform/android/power_android.cpp ./platform/osx/power_osx.cpp ./platform/windows/power_windows.cpp diff --git a/misc/scripts/clang_format.sh b/misc/scripts/clang_format.sh index cb007b0c6b7..bd728a6f66c 100755 --- a/misc/scripts/clang_format.sh +++ b/misc/scripts/clang_format.sh @@ -35,6 +35,8 @@ while IFS= read -rd '' f; do continue 2 elif [[ "$f" == "platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper"* ]]; then continue 2 + elif [[ "$f" == "platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix"* ]]; then + continue 2 fi python misc/scripts/copyright_headers.py "$f" continue 2 diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index cff3be5877d..8f5083df2e0 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -791,7 +791,6 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref &p encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]); } - if (tname == "instrumentation" && attrname == "targetPackage") { - string_table.write[attr_value] = get_package_name(package_name); - } - if (tname == "activity" && attrname == "screenOrientation") { encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]); } diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index f29f7a135a3..1e092fd6f6f 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -233,19 +233,6 @@ String _get_xr_features_tag(const Ref &p_preset) { return manifest_xr_features; } -String _get_instrumentation_tag(const Ref &p_preset) { - String package_name = p_preset->get("package/unique_name"); - String manifest_instrumentation_text = vformat( - " \n", - package_name); - return manifest_instrumentation_text; -} - String _get_activity_tag(const Ref &p_preset) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OVR || xr_mode_index == XR_MODE_OPENXR; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index bc84a1e3a4d..c9f84a400c8 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -103,8 +103,6 @@ String _get_screen_sizes_tag(const Ref &p_preset); String _get_xr_features_tag(const Ref &p_preset); -String _get_instrumentation_tag(const Ref &p_preset); - String _get_activity_tag(const Ref &p_preset); String _get_application_tag(const Ref &p_preset, bool p_has_storage_permission); diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 22603c534fe..90dc61a6acc 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -16,12 +16,13 @@ + + - - diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index fb1604f6af8..f21f88db0a9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -30,7 +30,8 @@ package org.godotengine.godot; -import android.content.ComponentName; +import org.godotengine.godot.utils.ProcessPhoenix; + import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -71,6 +72,7 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God @Override public void onDestroy() { + Log.v(TAG, "Destroying Godot app..."); super.onDestroy(); onGodotForceQuit(godotFragment); } @@ -78,27 +80,21 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God @Override public final void onGodotForceQuit(Godot instance) { if (instance == godotFragment) { - System.exit(0); + Log.v(TAG, "Force quitting Godot instance"); + ProcessPhoenix.forceQuit(this); } } @Override public final void onGodotRestartRequested(Godot instance) { if (instance == godotFragment) { - // HACK: - // - // Currently it's very hard to properly deinitialize Godot on Android to restart the game + // 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). - // - // Using instrumentation is a way of making the whole app process restart, because Android - // will kill any process of the same package which was already running. - // - Bundle args = new Bundle(); - args.putParcelable("intent", getIntent()); - startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args); + Log.v(TAG, "Restarting Godot instance..."); + ProcessPhoenix.triggerRebirth(this); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java deleted file mode 100644 index 44f0a3eb3e2..00000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java +++ /dev/null @@ -1,50 +0,0 @@ -/*************************************************************************/ -/* GodotInstrumentation.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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.Instrumentation; -import android.content.Intent; -import android.os.Bundle; - -public class GodotInstrumentation extends Instrumentation { - private Intent intent; - - @Override - public void onCreate(Bundle arguments) { - intent = arguments.getParcelable("intent"); - start(); - } - - @Override - public void onStart() { - startActivitySync(intent); - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java new file mode 100644 index 00000000000..2cc37b627a8 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -0,0 +1,141 @@ +// clang-format off + +/* Third-party library. + * Upstream: https://github.com/JakeWharton/ProcessPhoenix + * Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4 + */ + +/* + * Copyright (C) 2014 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.godotengine.godot.utils; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Process; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + +/** + * Process Phoenix facilitates restarting your application process. This should only be used for + * things like fundamental state changes in your debug builds (e.g., changing from staging to + * production). + *

+ * Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance. + */ +public final class ProcessPhoenix extends Activity { + private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; + + /** + * Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default} + * activity as an intent. + *

+ * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context) { + triggerRebirth(context, getRestartIntent(context)); + } + + /** + * Call to restart the application process using the specified intents. + *

+ * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Intent... nextIntents) { + if (nextIntents.length < 1) { + throw new IllegalArgumentException("intents cannot be empty"); + } + // create a new task for the first activity. + nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + + Intent intent = new Intent(context, ProcessPhoenix.class); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context. + intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents))); + intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()); + context.startActivity(intent); + } + + // -- GODOT start -- + /** + * Finish the activity and kill its process + */ + public static void forceQuit(Activity activity) { + forceQuit(activity, Process.myPid()); + } + + /** + * Finish the activity and kill its process + * @param activity + * @param pid + */ + public static void forceQuit(Activity activity, int pid) { + Process.killProcess(pid); // Kill original main process + activity.finish(); + Runtime.getRuntime().exit(0); // Kill kill kill! + } + + // -- GODOT end -- + + private static Intent getRestartIntent(Context context) { + String packageName = context.getPackageName(); + Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + if (defaultIntent != null) { + return defaultIntent; + } + + throw new IllegalStateException("Unable to determine default activity for " + + packageName + + ". Does an activity specify the DEFAULT category in its intent filter?"); + } + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // -- GODOT start -- + ArrayList intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS); + startActivities(intents.toArray(new Intent[intents.size()])); + forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); + // -- GODOT end -- + } + + /** + * Checks if the current process is a temporary Phoenix Process. + * This can be used to avoid initialisation of unused resources or to prevent running code that + * is not multi-process ready. + * + * @return true if the current process is a temporary Phoenix Process + */ + public static boolean isPhoenixProcess(Context context) { + int currentPid = Process.myPid(); + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List runningProcesses = manager.getRunningAppProcesses(); + if (runningProcesses != null) { + for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) { + if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) { + return true; + } + } + } + return false; + } +}