Skip to content

Commit 48fc23d

Browse files
committed
Add support for using an Android Service to host the Godot engine
- Provide an `GodotService` Android service implementation which can be used to host an instance of the Godot engine - Provide an `RemoteGodotFragment` Android fragment implementation which provides the view and logic to wrap connection to a `GodotService` instance
1 parent fde0616 commit 48fc23d

14 files changed

+1003
-156
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent">
5+
6+
<SurfaceView
7+
android:id="@+id/remote_godot_window_surface"
8+
android:layout_width="match_parent"
9+
android:layout_height="match_parent" />
10+
11+
</FrameLayout>

platform/android/java/lib/src/org/godotengine/godot/Godot.kt

+35-42
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ import java.util.concurrent.atomic.AtomicReference
9090
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
9191
* lifecycle methods are properly invoked.
9292
*/
93-
class Godot(private val context: Context) {
93+
class Godot(val context: Context) {
9494

9595
internal companion object {
9696
private val TAG = Godot::class.java.simpleName
@@ -177,7 +177,7 @@ class Godot(private val context: Context) {
177177
*/
178178
private val godotMainLoopStarted = AtomicBoolean(false)
179179

180-
var io: GodotIO? = null
180+
val io = GodotIO(this)
181181

182182
private var commandLine : MutableList<String> = ArrayList<String>()
183183
private var xrMode = XRMode.REGULAR
@@ -187,7 +187,7 @@ class Godot(private val context: Context) {
187187
private var useDebugOpengl = false
188188
private var darkMode = false
189189

190-
private var containerLayout: FrameLayout? = null
190+
internal var containerLayout: FrameLayout? = null
191191
var renderView: GodotRenderView? = null
192192

193193
/**
@@ -198,13 +198,12 @@ class Godot(private val context: Context) {
198198
/**
199199
* Returns true if the engine has been initialized, false otherwise.
200200
*/
201-
fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized
201+
fun isInitialized() = primaryHost != null && initializationStarted && isNativeInitialized() && renderViewInitialized
202202

203203
/**
204204
* Provides access to the primary host [Activity]
205205
*/
206206
fun getActivity() = primaryHost?.activity
207-
private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
208207

209208
/**
210209
* Start initialization of the Godot engine.
@@ -228,17 +227,13 @@ class Godot(private val context: Context) {
228227
beginBenchmarkMeasure("Startup", "Godot::onCreate")
229228
try {
230229
this.primaryHost = primaryHost
231-
val activity = requireActivity()
232-
val window = activity.window
233-
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
234230

235231
Log.v(TAG, "Initializing Godot plugin registry")
236232
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
237233
runtimePlugins.addAll(primaryHost.getHostPlugins(this))
238234
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
239-
if (io == null) {
240-
io = GodotIO(activity)
241-
}
235+
236+
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
242237

243238
// check for apk expansion API
244239
commandLine = getCommandLine()
@@ -264,7 +259,7 @@ class Godot(private val context: Context) {
264259
i++
265260
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
266261
mainPackKey = commandLine[i + 1]
267-
val prefs = activity.getSharedPreferences(
262+
val prefs = context.getSharedPreferences(
268263
"app_data_keys",
269264
Context.MODE_PRIVATE
270265
)
@@ -294,10 +289,10 @@ class Godot(private val context: Context) {
294289
// Build the full path to the app's expansion files
295290
try {
296291
expansionPackPath = Helpers.getSaveFilePath(context)
297-
expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
298-
activity.packageName,
292+
expansionPackPath += "/main." + context.packageManager.getPackageInfo(
293+
context.packageName,
299294
0
300-
).versionCode + "." + activity.packageName + ".obb"
295+
).versionCode + "." + context.packageName + ".obb"
301296
} catch (e: java.lang.Exception) {
302297
Log.e(TAG, "Unable to build full path to the app's expansion files", e)
303298
}
@@ -408,12 +403,11 @@ class Godot(private val context: Context) {
408403
commandLine.add("--main-pack")
409404
commandLine.add(expansionPackPath)
410405
}
411-
val activity = requireActivity()
412406
if (!nativeLayerInitializeCompleted) {
413407
nativeLayerInitializeCompleted = GodotLib.initialize(
414-
activity,
408+
getActivity(),
415409
this,
416-
activity.assets,
410+
context.assets,
417411
io,
418412
netUtils,
419413
directoryAccessHandler,
@@ -451,7 +445,7 @@ class Godot(private val context: Context) {
451445
* @throws IllegalStateException if [onInitNativeLayer] has not been called
452446
*/
453447
@JvmOverloads
454-
fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? {
448+
fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(context)): FrameLayout? {
455449
if (!isNativeInitialized()) {
456450
throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view")
457451
}
@@ -460,7 +454,6 @@ class Godot(private val context: Context) {
460454

461455
beginBenchmarkMeasure("Startup", "Godot::onInitRenderView")
462456
try {
463-
val activity: Activity = host.activity
464457
containerLayout = providedContainerLayout
465458
containerLayout?.removeAllViews()
466459
containerLayout?.layoutParams = ViewGroup.LayoutParams(
@@ -469,29 +462,29 @@ class Godot(private val context: Context) {
469462
)
470463

471464
// GodotEditText layout
472-
val editText = GodotEditText(activity)
465+
val editText = GodotEditText(context)
473466
editText.layoutParams =
474467
ViewGroup.LayoutParams(
475468
ViewGroup.LayoutParams.MATCH_PARENT,
476-
activity.resources.getDimension(R.dimen.text_edit_height).toInt()
469+
context.resources.getDimension(R.dimen.text_edit_height).toInt()
477470
)
478471
// Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer.
479472
editText.setBackgroundColor(Color.TRANSPARENT)
480473
// ...add to FrameLayout
481474
containerLayout?.addView(editText)
482475
renderView = if (usesVulkan()) {
483-
if (meetsVulkanRequirements(activity.packageManager)) {
484-
GodotVulkanRenderView(host, this, godotInputHandler)
476+
if (meetsVulkanRequirements(context.packageManager)) {
477+
GodotVulkanRenderView(this, godotInputHandler)
485478
} else if (canFallbackToOpenGL()) {
486479
// Fallback to OpenGl.
487-
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
480+
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl)
488481
} else {
489-
throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
482+
throw IllegalStateException(context.getString(R.string.error_missing_vulkan_requirements_message))
490483
}
491484

492485
} else {
493486
// Fallback to OpenGl.
494-
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
487+
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl)
495488
}
496489

497490
if (host == primaryHost) {
@@ -509,20 +502,21 @@ class Godot(private val context: Context) {
509502
}
510503

511504
editText.setView(renderView)
512-
io?.setEdit(editText)
505+
io.setEdit(editText)
513506

507+
val activity = host.activity
514508
// Listeners for keyboard height.
515-
val decorView = activity.window.decorView
509+
val topView = activity?.window?.decorView ?: providedContainerLayout
516510
// Report the height of virtual keyboard as it changes during the animation.
517-
ViewCompat.setWindowInsetsAnimationCallback(decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
511+
ViewCompat.setWindowInsetsAnimationCallback(topView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
518512
var startBottom = 0
519513
var endBottom = 0
520514
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
521-
startBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
515+
startBottom = ViewCompat.getRootWindowInsets(topView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
522516
}
523517

524518
override fun onStart(animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat): WindowInsetsAnimationCompat.BoundsCompat {
525-
endBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
519+
endBottom = ViewCompat.getRootWindowInsets(topView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
526520
return bounds
527521
}
528522

@@ -648,16 +642,17 @@ class Godot(private val context: Context) {
648642
}
649643

650644
fun onDestroy(primaryHost: GodotHost) {
651-
Log.v(TAG, "OnDestroy: $primaryHost")
652645
if (this.primaryHost != primaryHost) {
653646
return
654647
}
648+
Log.v(TAG, "OnDestroy: $primaryHost")
655649

656650
for (plugin in pluginRegistry.allPlugins) {
657651
plugin.onMainDestroy()
658652
}
659653

660654
renderView?.onActivityDestroyed()
655+
this.primaryHost = null
661656
}
662657

663658
/**
@@ -776,16 +771,16 @@ class Godot(private val context: Context) {
776771
@StringRes titleResId: Int,
777772
okCallback: Runnable?
778773
) {
779-
val res: Resources = getActivity()?.resources ?: return
774+
val res: Resources = context.resources ?: return
780775
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
781776
}
782777

783778
@JvmOverloads
784779
@Keep
785780
fun alert(message: String, title: String, okCallback: Runnable? = null) {
786-
val activity: Activity = getActivity() ?: return
781+
val context = getActivity() ?: context
787782
runOnUiThread {
788-
val builder = AlertDialog.Builder(activity)
783+
val builder = AlertDialog.Builder(context)
789784
builder.setMessage(message).setTitle(title)
790785
builder.setPositiveButton(
791786
R.string.dialog_ok
@@ -814,8 +809,7 @@ class Godot(private val context: Context) {
814809
* of the UI thread.
815810
*/
816811
fun runOnUiThread(action: Runnable) {
817-
val activity: Activity = getActivity() ?: return
818-
activity.runOnUiThread(action)
812+
primaryHost?.runOnHostThread(action)
819813
}
820814

821815
/**
@@ -968,8 +962,7 @@ class Godot(private val context: Context) {
968962
@JvmOverloads
969963
fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
970964
val host = primaryHost
971-
val activity = host?.activity
972-
if (host == null || activity == null) {
965+
if (host == null) {
973966
// Run the destroyRunnable right away as we are about to force quit.
974967
destroyRunnable?.run()
975968

@@ -1038,7 +1031,7 @@ class Godot(private val context: Context) {
10381031

10391032
private fun getCommandLine(): MutableList<String> {
10401033
val commandLine = try {
1041-
commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
1034+
commandLineFileParser.parseCommandLine(context.assets.open("_cl_"))
10421035
} catch (ignored: Exception) {
10431036
mutableListOf()
10441037
}
@@ -1070,7 +1063,7 @@ class Godot(private val context: Context) {
10701063
}
10711064

10721065
fun getGrantedPermissions(): Array<String?>? {
1073-
return PermissionsUtil.getGrantedPermissions(getActivity())
1066+
return PermissionsUtil.getGrantedPermissions(context)
10741067
}
10751068

10761069
/**

platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,14 @@
7777
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
7878
*/
7979
class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
80-
private final GodotHost host;
8180
private final Godot godot;
8281
private final GodotInputHandler inputHandler;
8382
private final GodotRenderer godotRenderer;
8483
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
8584

86-
public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) {
87-
super(host.getActivity());
85+
public GodotGLRenderView(Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) {
86+
super(godot.getContext());
8887

89-
this.host = host;
9088
this.godot = godot;
9189
this.inputHandler = inputHandler;
9290
this.godotRenderer = new GodotRenderer();

platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import android.app.Activity;
3737

3838
import androidx.annotation.NonNull;
39+
import androidx.annotation.Nullable;
3940

4041
import java.util.Collections;
4142
import java.util.List;
@@ -96,8 +97,9 @@ default int onNewGodotInstanceRequested(String[] args) {
9697
}
9798

9899
/**
99-
* Provide access to the Activity hosting the {@link Godot} engine.
100+
* Provide access to the Activity hosting the {@link Godot} engine if any.
100101
*/
102+
@Nullable
101103
Activity getActivity();
102104

103105
/**
@@ -150,4 +152,18 @@ default boolean supportsFeature(String featureTag) {
150152
* Invoked on the render thread when an editor workspace has been selected.
151153
*/
152154
default void onEditorWorkspaceSelected(String workspace) {}
155+
156+
/**
157+
* Runs the specified action on a host provided thread.
158+
*/
159+
default void runOnHostThread(Runnable action) {
160+
if (action == null) {
161+
return;
162+
}
163+
164+
Activity activity = getActivity();
165+
if (activity != null) {
166+
activity.runOnUiThread(action);
167+
}
168+
}
153169
}

0 commit comments

Comments
 (0)