diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java index 3bad1ea0..dbd409e8 100644 --- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java +++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java @@ -143,16 +143,36 @@ public class GameActivity extends SDLActivity { private static native void nativeSetDefaultStreamValues(int sampleRate, int framesPerBurst); + /** + * Native libraries required by an optional Android host extension. + * + * Subclasses supplied by another product flavor may override this method. + * The libraries are loaded after LÖVE's dependencies and before liblove; + * liblove must remain last because SDL treats the final entry as the main + * shared object. + */ + protected String[] getHostLibraries() { + return new String[0]; + } + @Override protected String[] getLibraries() { - return new String[] { - "c++_shared", - "mpg123", - "openal", - "love", - }; + String[] hostLibraries = getHostLibraries(); + String[] libraries = new String[hostLibraries.length + 4]; + libraries[0] = "c++_shared"; + libraries[1] = "mpg123"; + libraries[2] = "openal"; + System.arraycopy(hostLibraries, 0, libraries, 3, hostLibraries.length); + libraries[libraries.length - 1] = "love"; + return libraries; } + protected void onHostCreateBeforeSDL(Bundle savedInstanceState) {} + protected void onHostCreateAfterSDL(Bundle savedInstanceState) {} + protected void onHostResume() {} + protected void onHostPause() {} + protected void onHostDestroy() {} + @Override protected String getMainSharedObject() { String[] libs = getLibraries(); @@ -192,7 +212,9 @@ public class GameActivity extends SDLActivity { intent.setData(null); } + onHostCreateBeforeSDL(savedInstanceState); super.onCreate(savedInstanceState); + onHostCreateAfterSDL(savedInstanceState); if (savedInstanceState != null) { // Restore the in-flight SAF destinations, so a pick that returns to // a recreated activity still lands under the basename it asked for. @@ -341,6 +363,7 @@ public class GameActivity extends SDLActivity { Log.d("GameActivity", "Cancelling vibration"); vibrator.cancel(); } + onHostDestroy(); super.onDestroy(); } @@ -351,12 +374,14 @@ public class GameActivity extends SDLActivity { vibrator.cancel(); } teardownSecondaryDisplay(); + onHostPause(); super.onPause(); } @Override public void onResume() { super.onResume(); + onHostResume(); setupSecondaryDisplay(); } diff --git a/tests/engine/android_host_extension_test.lua b/tests/engine/android_host_extension_test.lua new file mode 100644 index 00000000..14d7ae5a --- /dev/null +++ b/tests/engine/android_host_extension_test.lua @@ -0,0 +1,58 @@ +-- GameActivity exposes an optional Android host seam without requiring any +-- host implementation (or its native libraries) in the stock build. +-- Self-contained: luajit tests/engine/android_host_extension_test.lua +local path = "mobile/android/love/src/main/java/org/love2d/android/GameActivity.java" +local file = assert(io.open(path, "rb")) +local source = file:read("*a") +file:close() + +local function check(value, message) + if not value then error(message, 2) end +end + +local function position(text) + local p = source:find(text, 1, true) + check(p, "missing: " .. text) + return p +end + +check(source:find("protected String[] getHostLibraries()", 1, true), + "host libraries are an overridable protected extension") +check(source:find("return new String[0];", 1, true), + "the vanilla host library list is empty") + +local cpp = position('libraries[0] = "c++_shared";') +local mpg = position('libraries[1] = "mpg123";') +local openal = position('libraries[2] = "openal";') +local host = position("System.arraycopy(hostLibraries, 0, libraries, 3, hostLibraries.length);") +local love = position('libraries[libraries.length - 1] = "love";') +check(cpp < mpg and mpg < openal and openal < host and host < love, + "optional libraries load after dependencies while liblove remains last") + +for _, hook in ipairs({ + "onHostCreateBeforeSDL", "onHostCreateAfterSDL", "onHostResume", + "onHostPause", "onHostDestroy", +}) do + check(source:find("protected void " .. hook, 1, true), + hook .. " is a protected extension hook") +end + +check(position("onHostCreateBeforeSDL(savedInstanceState);") < + position("super.onCreate(savedInstanceState);") and + position("super.onCreate(savedInstanceState);") < + position("onHostCreateAfterSDL(savedInstanceState);"), + "create hooks bracket SDL creation") +check(position("super.onResume();") < position("onHostResume();"), + "resume hook runs after SDL resumes") +check(position("onHostPause();") < position("super.onPause();"), + "pause hook runs before SDL pauses") +check(position("onHostDestroy();") < position("super.onDestroy();"), + "destroy hook runs before SDL destruction") + +check(not source:lower():find("openxr", 1, true), + "generic Android activity must not require OpenXR") +check(not source:find("QuestActivity", 1, true) and + not source:find("QuestBridge", 1, true), + "generic Android activity must not require Quest classes") + +print("android_host_extension_test: ok")