From 961394a988c7567612b133092212cbacf4dd98b2 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Thu, 7 Mar 2024 19:16:25 -0800 Subject: [PATCH] Add support for launching the Play window in PiP mode --- doc/classes/EditorSettings.xml | 12 +- editor/editor_settings.cpp | 6 + .../java/editor/src/main/AndroidManifest.xml | 5 +- .../editor/EditorMessageDispatcher.kt | 192 ++++++++++++++++++ .../godotengine/editor/EditorWindowInfo.kt | 27 +-- .../org/godotengine/editor/GodotEditor.kt | 155 +++++++++++--- .../java/org/godotengine/editor/GodotGame.kt | 93 ++++++++- .../drawable/ic_play_window_foreground.xml | 25 +++ .../drawable/outline_fullscreen_exit_48.xml | 12 ++ .../pip_button_activated_bg_drawable.xml | 10 + .../res/drawable/pip_button_bg_drawable.xml | 9 + .../pip_button_default_bg_drawable.xml | 10 + .../src/main/res/layout/godot_game_layout.xml | 25 +++ .../res/mipmap-anydpi-v26/ic_play_window.xml | 5 + .../main/res/mipmap-hdpi/ic_play_window.png | Bin 0 -> 1954 bytes .../main/res/mipmap-mdpi/ic_play_window.png | Bin 0 -> 1350 bytes .../main/res/mipmap-xhdpi/ic_play_window.png | Bin 0 -> 2617 bytes .../main/res/mipmap-xxhdpi/ic_play_window.png | Bin 0 -> 3923 bytes .../res/mipmap-xxxhdpi/ic_play_window.png | Bin 0 -> 5480 bytes .../editor/src/main/res/values/dimens.xml | 2 +- .../editor/src/main/res/values/strings.xml | 2 + .../org/godotengine/godot/GodotActivity.kt | 8 - .../godot/utils/ProcessPhoenix.java | 27 ++- 23 files changed, 568 insertions(+), 57 deletions(-) create mode 100644 platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt create mode 100644 platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml create mode 100644 platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml create mode 100644 platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml create mode 100644 platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml create mode 100644 platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml create mode 100644 platform/android/java/editor/src/main/res/layout/godot_game_layout.xml create mode 100644 platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml create mode 100644 platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png create mode 100644 platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png create mode 100644 platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png create mode 100644 platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png create mode 100644 platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 4bb7149f2f..52ea9fc1ea 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -961,7 +961,17 @@ If [code]true[/code], on Linux/BSD, the editor will check for Wayland first instead of X11 (if available). - The Android window to display the project on when starting the project from the editor. + Specifies how the Play window is launched relative to the Android editor. + - [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets. + - [b]Same as Editor[/b] will launch the Play window in the same window as the Editor. + - [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window. + [b]Note:[/b] Only available in the Android editor. + + + Specifies the picture-in-picture (PiP) mode for the Play window. + - [b]Disabled:[/b] PiP is disabled for the Play window. + - [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode. + - [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode. [b]Note:[/b] Only available in the Android editor. diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index a4cb3fbb68..c24b70f035 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -825,6 +825,12 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2"; EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints) + int default_play_window_pip_mode = 0; +#ifdef ANDROID_ENABLED + default_play_window_pip_mode = 2; +#endif + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2") + // Auto save _initial_set("run/auto_save/save_before_running", true); diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index c7d14a3f49..a875745860 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:exported="true" + android:icon="@mipmap/icon" android:launchMode="singleTask" android:screenOrientation="userLandscape"> () + + @SuppressLint("HandlerLeak") + private val dispatcherHandler = object : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_FORCE_QUIT -> editor.finish() + + MSG_REGISTER_MESSENGER -> { + val editorId = msg.arg1 + val messenger = msg.replyTo + registerMessenger(editorId, messenger) + } + + else -> super.handleMessage(msg) + } + } + } + + /** + * Request the window with the given [editorId] to force quit. + */ + fun requestForceQuit(editorId: Int): Boolean { + val messenger = recipientsMessengers[editorId] ?: return false + return try { + Log.v(TAG, "Requesting 'forceQuit' for $editorId") + val msg = Message.obtain(null, MSG_FORCE_QUIT) + messenger.send(msg) + true + } catch (e: RemoteException) { + Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e) + recipientsMessengers.remove(editorId) + false + } + } + + /** + * Utility method to register a receiver messenger. + */ + private fun registerMessenger(editorId: Int, messenger: Messenger?, messengerDeathCallback: Runnable? = null) { + try { + if (messenger == null) { + Log.w(TAG, "Invalid 'replyTo' payload") + } else if (messenger.binder.isBinderAlive) { + messenger.binder.linkToDeath({ + Log.v(TAG, "Removing messenger for $editorId") + recipientsMessengers.remove(editorId) + messengerDeathCallback?.run() + }, 0) + recipientsMessengers[editorId] = messenger + } + } catch (e: RemoteException) { + Log.e(TAG, "Unable to register messenger from $editorId", e) + recipientsMessengers.remove(editorId) + } + } + + /** + * Utility method to register a [Messenger] attached to this handler with a host. + * + * This is done so that the host can send request to the editor instance attached to this handle. + * + * Note that this is only done when the editor instance is internal (not exported) to prevent + * arbitrary apps from having the ability to send requests. + */ + private fun registerSelfTo(pm: PackageManager, host: Messenger?, selfId: Int) { + try { + if (host == null || !host.binder.isBinderAlive) { + Log.v(TAG, "Host is unavailable") + return + } + + val activityInfo = pm.getActivityInfo(editor.componentName, 0) + if (activityInfo.exported) { + Log.v(TAG, "Not registering self to host as we're exported") + return + } + + Log.v(TAG, "Registering self $selfId to host") + val msg = Message.obtain(null, MSG_REGISTER_MESSENGER) + msg.arg1 = selfId + msg.replyTo = Messenger(dispatcherHandler) + host.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to register self with host", e) + } + } + + /** + * Parses the starting intent and retrieve an editor messenger if available + */ + fun parseStartIntent(pm: PackageManager, intent: Intent) { + val messengerBundle = intent.getBundleExtra(EXTRA_MSG_DISPATCHER_PAYLOAD) ?: return + + // Retrieve the sender messenger payload and store it. This can be used to communicate back + // to the sender. + val senderId = messengerBundle.getInt(KEY_EDITOR_ID) + val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER) + registerMessenger(senderId, senderMessenger) + + // Register ourselves to the sender so that it can communicate with us. + registerSelfTo(pm, senderMessenger, editor.getEditorId()) + } + + /** + * Returns the payload used by the [EditorMessageDispatcher] class to establish an IPC bridge + * across editor instances. + */ + fun getMessageDispatcherPayload(): Bundle { + return Bundle().apply { + putInt(KEY_EDITOR_ID, editor.getEditorId()) + putParcelable(KEY_EDITOR_MESSENGER, Messenger(dispatcherHandler)) + } + } +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt index 0da1d01aed..d3daa1dbbc 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt @@ -31,23 +31,24 @@ package org.godotengine.editor /** - * Specifies the policy for adjacent launches. + * Specifies the policy for launches. */ -enum class LaunchAdjacentPolicy { +enum class LaunchPolicy { /** - * Adjacent launches are disabled. - */ - DISABLED, - - /** - * Adjacent launches are enabled / disabled based on the device and screen metrics. + * Launch policy is determined by the editor settings or based on the device and screen metrics. */ AUTO, + + /** + * Launches happen in the same window. + */ + SAME, + /** * Adjacent launches are enabled. */ - ENABLED + ADJACENT } /** @@ -57,12 +58,14 @@ data class EditorWindowInfo( val windowClassName: String, val windowId: Int, val processNameSuffix: String, - val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED + val launchPolicy: LaunchPolicy = LaunchPolicy.SAME, + val supportsPiPMode: Boolean = false ) { constructor( windowClass: Class<*>, windowId: Int, processNameSuffix: String, - launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED - ) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy) + launchPolicy: LaunchPolicy = LaunchPolicy.SAME, + supportsPiPMode: Boolean = false + ) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode) } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 9cc133046b..5d6da06f97 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -32,6 +32,7 @@ package org.godotengine.editor import android.Manifest import android.app.ActivityManager +import android.app.ActivityOptions import android.content.ComponentName import android.content.Context import android.content.Intent @@ -69,17 +70,24 @@ open class GodotEditor : GodotActivity() { private const val WAIT_FOR_DEBUGGER = false - private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic + protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic + protected val EXTRA_PIP_AVAILABLE = "pip_available" + @JvmStatic + protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested" // Command line arguments private const val EDITOR_ARG = "--editor" private const val EDITOR_ARG_SHORT = "-e" private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" + private const val BREAKPOINTS_ARG = "--breakpoints" + private const val BREAKPOINTS_ARG_SHORT = "-b" // Info for the various classes used by the editor internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") - internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchAdjacentPolicy.AUTO) + internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true) /** * Sets of constants to specify the window to use to run the project. @@ -90,13 +98,26 @@ open class GodotEditor : GodotActivity() { private const val ANDROID_WINDOW_AUTO = 0 private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1 private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2 + + /** + * Sets of constants to specify the Play window PiP mode. + * + * Should match the values in `editor/editor_settings.cpp'` for the + * 'run/window_placement/play_window_pip_mode' setting. + */ + private const val PLAY_WINDOW_PIP_DISABLED = 0 + private const val PLAY_WINDOW_PIP_ENABLED = 1 + private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2 } + private val editorMessageDispatcher = EditorMessageDispatcher(this) private val commandLineParams = ArrayList() private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } override fun getGodotAppLayout() = R.layout.godot_editor_layout + internal open fun getEditorId() = EDITOR_MAIN_INFO.windowId + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() @@ -108,6 +129,8 @@ open class GodotEditor : GodotActivity() { Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") updateCommandLineParams(params?.asList() ?: emptyList()) + editorMessageDispatcher.parseStartIntent(packageManager, intent) + if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { Debug.waitForDebugger() } @@ -189,35 +212,67 @@ open class GodotEditor : GodotActivity() { } } - override fun onNewGodotInstanceRequested(args: Array): Int { - val editorWindowInfo = getEditorWindowInfo(args) - - // Launch a new activity + protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array): Intent { val newInstance = Intent() .setComponent(ComponentName(this, editorWindowInfo.windowClassName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.ENABLED || - (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.AUTO && shouldGameLaunchAdjacent())) { + + val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy) + val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) { + val pipMode = getPlayWindowPiPMode() + pipMode == PLAY_WINDOW_PIP_ENABLED || + (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME) + } else { + false + } + newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable) + + if (launchPolicy == LaunchPolicy.ADJACENT) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Log.v(TAG, "Adding flag for adjacent launch") newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) } + } else if (launchPolicy == LaunchPolicy.SAME) { + if (isPiPAvailable && + (args.contains(BREAKPOINTS_ARG) || args.contains(BREAKPOINTS_ARG_SHORT))) { + Log.v(TAG, "Launching in PiP mode because of breakpoints") + newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true) + } } + + return newInstance + } + + override fun onNewGodotInstanceRequested(args: Array): Int { + val editorWindowInfo = getEditorWindowInfo(args) + + // Launch a new activity + val sourceView = godotFragment?.view + val activityOptions = if (sourceView == null) { + null + } else { + val startX = sourceView.width / 2 + val startY = sourceView.height / 2 + ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0) + } + + val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args) if (editorWindowInfo.windowClassName == javaClass.name) { Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") val godot = godot if (godot != null) { godot.destroyAndKillProcess { - ProcessPhoenix.triggerRebirth(this, newInstance) + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) } } else { - ProcessPhoenix.triggerRebirth(this, newInstance) + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) } } else { Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") newInstance.putExtra(EXTRA_NEW_LAUNCH, true) - startActivity(newInstance) + .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload()) + startActivity(newInstance, activityOptions?.toBundle()) } return editorWindowInfo.windowId } @@ -231,6 +286,12 @@ open class GodotEditor : GodotActivity() { return true } + // Send an inter-process message to request the target editor window to force quit. + if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) { + return true + } + + // Fallback to killing the target process. val processName = packageName + editorWindowInfo.processNameSuffix val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val runningProcesses = activityManager.runningAppProcesses @@ -285,29 +346,65 @@ open class GodotEditor : GodotActivity() { java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) /** - * Whether we should launch the new godot instance in an adjacent window - * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT + * Retrieves the play window pip mode editor setting. */ - private fun shouldGameLaunchAdjacent(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - try { - when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { - ANDROID_WINDOW_SAME_AS_EDITOR -> false - ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true - else -> { - // ANDROID_WINDOW_AUTO - isInMultiWindowMode || isLargeScreen - } - } - } catch (e: NumberFormatException) { - // Fall-back to the 'Auto' behavior - isInMultiWindowMode || isLargeScreen - } + private fun getPlayWindowPiPMode(): Int { + return try { + Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/play_window_pip_mode")) + } catch (e: NumberFormatException) { + PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR + } + } + + /** + * If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the + * editor setting or device and screen metrics. + * + * If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default + * launch policy. + */ + private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy { + val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isInMultiWindowMode } else { false } + val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen) { + LaunchPolicy.ADJACENT + } else { + LaunchPolicy.SAME + } + + return when (policy) { + LaunchPolicy.AUTO -> { + try { + when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { + ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME + ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT + else -> { + // ANDROID_WINDOW_AUTO + defaultLaunchPolicy + } + } + } catch (e: NumberFormatException) { + Log.w(TAG, "Error parsing the Android window placement editor setting", e) + // Fall-back to the default launch policy + defaultLaunchPolicy + } + } + + else -> { + policy + } + } } + /** + * Returns true the if the device supports picture-in-picture (PiP) + */ + protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // Check if we got the MANAGE_EXTERNAL_STORAGE permission diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 2bcfba559c..33fcbf9030 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -30,6 +30,14 @@ package org.godotengine.editor +import android.annotation.SuppressLint +import android.app.PictureInPictureParams +import android.content.Intent +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View import org.godotengine.godot.GodotLib /** @@ -37,7 +45,90 @@ import org.godotengine.godot.GodotLib */ class GodotGame : GodotEditor() { - override fun getGodotAppLayout() = org.godotengine.godot.R.layout.godot_app_layout + companion object { + private val TAG = GodotGame::class.java.simpleName + } + + private val gameViewSourceRectHint = Rect() + private val pipButton: View? by lazy { + findViewById(R.id.godot_pip_button) + } + + private var pipAvailable = false + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val gameView = findViewById(R.id.godot_fragment_container) + gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + gameView.getGlobalVisibleRect(gameViewSourceRectHint) + } + } + + pipButton?.setOnClickListener { enterPiPMode() } + + handleStartIntent(intent) + } + + override fun onNewIntent(newIntent: Intent) { + super.onNewIntent(newIntent) + handleStartIntent(newIntent) + } + + private fun handleStartIntent(intent: Intent) { + pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable) + updatePiPButtonVisibility() + + val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false) + if (pipLaunchRequested) { + enterPiPMode() + } + } + + private fun updatePiPButtonVisibility() { + pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) { + View.VISIBLE + } else { + View.GONE + } + } + + private fun enterPiPMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setSeamlessResizeEnabled(false) + } + setPictureInPictureParams(builder.build()) + } + + Log.v(TAG, "Entering PiP mode") + enterPictureInPictureMode() + } + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode) + Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode") + updatePiPButtonVisibility() + } + + override fun onStop() { + super.onStop() + + val isInPiPMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode + if (isInPiPMode && !isFinishing) { + // We get in this state when PiP is closed, so we terminate the activity. + finish() + } + } + + override fun getGodotAppLayout() = R.layout.godot_game_layout + + override fun getEditorId() = RUN_GAME_INFO.windowId override fun overrideOrientationRequest() = false diff --git a/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml new file mode 100644 index 0000000000..41bc5475c8 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml new file mode 100644 index 0000000000..c8b5a15d19 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml new file mode 100644 index 0000000000..aeaa96ce54 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml new file mode 100644 index 0000000000..e9b2959275 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml new file mode 100644 index 0000000000..a8919689fe --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml new file mode 100644 index 0000000000..d53787c87e --- /dev/null +++ b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml new file mode 100644 index 0000000000..a3aabf2ee0 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png new file mode 100644 index 0000000000000000000000000000000000000000..a5ce40241f55d0a11a41b7e42406b7f09ae245f5 GIT binary patch literal 1954 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 literal 0 HcmV?d00001 diff --git a/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png new file mode 100644 index 0000000000000000000000000000000000000000..39d745039076f258d651bcba8110dab63f3a2551 GIT binary patch literal 3923 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 literal 0 HcmV?d00001 diff --git a/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png new file mode 100644 index 0000000000000000000000000000000000000000..b7a09a15b50c101541aef7e03717a7145f3792d4 GIT binary patch literal 5480 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Bd2>3_*8t*cli^^*vo2Ln`9l&Sjq=diCxx z{Zp5Odm9~_}Jq_0|_3s z=7R}DQ45_!vM#CoeKlpv&eqr4WM(+r;WlB=br4?V#TC)vG~M`g!i>Z5nqmgV6|L+4 zy?&ed^68`?0DJHOqYSvz@mir%C*)8bD5$7-UA zgCzP@O6r6sS@lQ0dVTORgNl&{vr3ta>ZXZ?#6n9YwttBAnYc>-%c}WrVs9%81z$EiygaQ%;I>56q*=Sx z=KlZuRQm63#w}rnPk$!K{bz}svv|V$q^{7+dyoAp58f=$=5;0Q%KMt1?a`(! z*R-zW2g%&p9n|%ab3bEOc-^+&u_r;cH?nD5-*EKsg;|Sv9_*eWGLFyASt|GeKJ+m6{HNn;9|Lg3Ml zY#(BIcpVlwEnqxlRbb9GtQ zJtO_MZTceqU*={$2|T;~@oBN)W9GNVHm~)@@5|@a5_k08b@%V=lRVvH6nOfi<^={pm8X9_lRnLgRZ>?!uEMsx zC~n%(A5*Q5+8qMs!e{t6Z>Ir z@-v={2Ad0yPFFsP`Br(ESM8wX7T^UI8{|@jS!3KH3G%_h)Z8HO0>S--X;c z7xnm5uANoA@}#^(Rz1(3dw<%$if2_5Yvc0G_FUhxb=jAVEo>V&kNp+Tu}s)CF>Jxi zCAZ)FY!;F2voHAbMbzZ)7wO9zFD`nyeDl!{uF*$#-E}`Ibkp;1$vL)<3F|eNJ$7Og zUVFdzN{C$8nVVbAPqCYJfx)cu%VWt$9XZ!lmQUYy&eXR!1bsS0eK(vnaj#|{a>PQIQMkdTK4{K_D4Hhn%FjY8b^A+>iL^*U2^Az z&gz%N%JbMNLd!0kPkTCVdHlK5ZnnK|Z8;16%v-!L{7BN16_Gsii?mD{eB*v3t$V2! zwK46wVDPPkBbOPhUeBpS+^37 zoMtFZna_4s=Ii@aXYTKbzq0wJ;W3q&RdU(^%d&1oI5OXGDSyj%| z)nEVGG<|!9+}{avv+Xs4cb{GD;mCYrLE`+CN!JB8dxVyqN|ySz?YmQj4Q~VIj+@fo zB4;r?a(}yi*~Nz&{nI*{)t&OW1pF#pxug!T{g<#fzT)mx=@m&=GVUyK6 zo~m=y)0Qk?%vo@MnQe_G?~(rLhXNs>f@7RK48hovEzh%q#d$s2B z*QkwE*ZY@k+1S8#f#vih^LN~L7araqe~tU+<%0|n8EtQwnv0{heLuU{pkn5+nm1Du zo<5ELS3m3Jti_z4rd~>(v_t!dnRwH*hKo5X#O-t#sgV*Ssf#c~eQt?x_fr`2vhzRB%)z_aM& zXw#xz2a*gL)_9cXI(C~uw8hvHL$x!mG4z=@WB=Bv%=c{BsB0ZU%y#ozkG+q z(M4j%z4c#h)VksSb?d~9^`7(CF1VD7zL@d!q3)Yg{A?B_N?*S7+Rfj6a!TH>U3Zr~ zUXvl;9=gz^K`s8={hVEiVI^}nT$&QbzFeB&?ELxX(`K&oXT806#oU|wHt#c*O+PBW zspsl`>D&wk-wQ8;^Jj?OF!1tS?d)iHV8_%n<~=4kjV~>^uIP zow>#&dG6D$K>Oo<^F_aZosr`8{e`-5Q`x+~3wo5dvU!S2IWS*Iu(F<~d#B3a&3VSB z?92~T|IggX(=0dr(fb5L9xj7@`!nlbc)Cm}+~I$rU?HQ)izyp*GR{6vKXofMQQ!Mm zQHlgd_G9MCB8NX^J{y?S4y^7w&7^iHZQXt05+3ay9Xt`D>Vb|0{~A^&{gz2Nlz*|Zk*hNobMh`=EHeztpCWp`_T8LsyDn$gN98Mhl#aZXbt}V(Ib*@^ zv-jB&-z(u6`w6Fzhu5P*5 z!p6avwQjQ1>B^5vHfi2hzwAngo=~X$<(_OG`;KjUa$=i87ls|)qowiMtu|@9GYhXk z)Amc#qxP*>WR!Y#-3IkS1D07kYj$4?$lh~cYwC&YBU@KpU(Y&Hc#B*c-LUG`{mb|2oO*dDmHDkLj7uJwx`S@?Cr�uMLWy$~19}!%RIV_NnXvpz>Oyg*Bq!hJzqvGu zci#`^5SLQmuwA}xP zriB*-ySL4{wCX{Oyia7ZmppGqg96WtwcEK&Zm>TIc)f6Q#`b6S`q>Bl)7F~1MP2Dn zcbER-yXQpS?^(rr7CRrD=Erj_Mzm{F_+>X{DFu!n0b8n)*Hjd)_`_|Cr#or%W zY#9@$u$b1{9DUEO_F}cApajRK9#8$%ikHsKtSV+SX)yoP%D8OZ-Xd9nS%N>-OgP68 zq#$6w@6w{n3;RTOOe^qd*t3`?KX^zM*ikTsm!r`>mQf~>=vmY(TJ zYU^*NM*p#}oAXp;Ba>{cPDt>2Yl#z=mzY}j?wu517V%+u=)6rz2c&eqN-bjK6=>Q2 zb$04*PGLTQQx5Fi_xY_hM4I)=@)=0kZCL*AZ-xVN%Ym%ZpFEg89)DFhKir_lGUMH` z72#ageDh8IzRhrCZaFZ6dxNFWBJQlLS_UnBQqfGNF8Rc&tD4%$Bzo>5p1Mi0$$3Ljp$*sL-vU|bH3)Q!7&A3){k?Avw z#&i!GPQUqQg>09t>8mWBz;=P<@^1;Tv!^FB-0prX<2U0;mxuwQ*;(6Sy+5ZIXLBv@ zji0!D`n$ueY#SI`Zl}Fq+W2ewn#1EN4<0 zzgc`knu1+-*H*U*Zy#HKorR2_4uBfn`|6s?4D9;0Cmm1RnDaZ)F!yer^OuOrdEfWm zY+{Jx&$WFLy?DKG^549TG1E8?h^tkftUrADk>6SK7mIj#9b)K-eg7FbOh5D4grQTV z&)lK)mv7hU`$1D@mF=)z>3u9xs=;N_5xrzRhv4m7em;rW$v&@+CF*nNu5$(sN4qqS zKFL2REo~EQyt4RL-Hxej3KN;wChQWr`tR!Jf}2+Se*&}ChO}~h4SZ>Jh#|bv#b|-f_lH?L@W-y#tNum( zo3`@juLld=ggN?kyLkkY*!MGL?fJEH#g~-dofl^KXSdeg>aLbx`{Da^*Yy+kjW)@D z@CjPJo^>9R=iK8lA;vBpvXg?0^`zeZ534=$mEY#JnAGlVNuRb}nB!kv$iLr8~*G|72V9RUg4c&HmOmP4E9$zv9xG$8IavYQ^1BJWX9R$ zmhO@ZW*jd*n{RWp`@Y8i-_NxCOJ$0pcl)~rE&tMOz&M}9WxDsH>1XG^(yi|7s?VLw z=Ucf|)if>RtR2rGj`9sFlph+M@J2rTKEjMu${F$#lR`|J|N>|($RH|jH|qb@~i$l z4cxxvjc(Q+vl|&glIOlPPE@jy?dWb~GdTDDGKO)|*Y^TH(m z;1|hG$rolRTOIhvk-=7JAQiGM<6?a1xOUx9TG`s;k*8n(ezL5V-M3F?w0y1eR{x6GzW8eNw{xrRy*e!Ov&F*y4*R>k zR~){bJzs9m<$LE##_h<|_mzt#UpXb)_G4)o53i$loUi4kZ{OShYc@#kiuXL_k+OVg z#@3ce*AJaEaLo0bd#IKB`u=y%Zuw7H_dB5b(pRZ%N_tD*Ij{A;Ub}&H3xltN^o5Rv zqRz&yfuEBDSfiSjEzotp_V3?^vz!wBn#pN59-nmxy_vsK(Rgaiq9a=k95>zPX;G=z z`rv9COVr1#XOn&`RLwYLnEZbytKPX8lHX#?7_IaqBv-vS7%=U)vV3NS*i;`sjY3~p z&sa~}M_vW*moj>(ISR++N=P+^Es)Gm5BSq3_4A}IlW&8&SlU~IvTjc!p5(hKE1!I{ zl9_olxeq)v*Pyn5H-jarF>QfthQ|Gl`|@?d3-*Q`c|3Qgvozn#rqvS1&x7h8<~cK8 zU@~FMbx^+GwQu#;V(t`$51kD?i5lE9k38m8xO}Ac$9sEl7&V&K?Gvw0V39h=Z{+Q< rK#gn8hvruM%}4Iy9hv+j_n$e<)&9Pk=Ic}j1_lOCS3j3^P6 - 600dp + 640dp 1024dp diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index 909711ab18..0ad54ac3a1 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ + Godot Play window Missing storage access permission! + Button used to toggle picture-in-picture mode for the Play window diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index 913e3d04c5..474c6e9b2f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -52,8 +52,6 @@ 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" } @@ -128,12 +126,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } 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) { 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 index b1bce45fbb..d9afdf90b1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -24,6 +24,7 @@ package org.godotengine.godot.utils; import android.app.Activity; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; */ public final class ProcessPhoenix extends Activity { private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + // -- GODOT start -- + private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options"; + // -- GODOT end -- private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; /** @@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity { triggerRebirth(context, getRestartIntent(context)); } + // -- GODOT start -- /** * 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) { + triggerRebirth(context, null, nextIntents); + } + + /** + * Call to restart the application process using the specified intents launched with the given + * {@link ActivityOptions}. + *

+ * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) { if (nextIntents.length < 1) { throw new IllegalArgumentException("intents cannot be empty"); } @@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity { 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()); + if (activityOptions != null) { + intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions); + } context.startActivity(intent); } - // -- GODOT start -- /** * Finish the activity and kill its process */ @@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity { 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)); + Intent launchIntent = getIntent(); + ArrayList intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS); + Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS); + startActivities(intents.toArray(new Intent[intents.size()]), activityOptions); + forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // -- GODOT end -- }