Skip to content

Add Android Game-Mode API listener support (#1995) - #2961

Open
jaime-jmebot wants to merge 3 commits into
jMonkeyEngine:masterfrom
jaime-jmebot:jaime/issue-1995-6df48d20
Open

jaime-jmebot wants to merge 3 commits into
jMonkeyEngine:masterfrom
jaime-jmebot:jaime/issue-1995-6df48d20

Conversation

@jaime-jmebot

@jaime-jmebot jaime-jmebot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Tracks #1995

Implementation plan

  • Prepare the bot fork and implementation branch
  • Add GameMode enum mirroring the Android 12 game-mode constants
  • Add OnGameModeChanged listener interface
  • Add reflection-based AndroidGameMode bridge (no API 31 compile/runtime dependency)
  • Wire setOnGameModeChanged() into JmeSurfaceView (register, report, unregister on destroy)
  • Wire setOnGameModeChanged() into AndroidHarnessFragment via the onAttach context
  • Add self-contained TestGameModeActivity example and AndroidManifest entry
  • Compile jme3-core + jme3-android + the example with javac (Gradle unavailable in sandbox)
  • Run and validate tests
  • Publish the validated patch
  • Mark this pull request ready for review

Progress summary

Implement Android Game-Mode API support in jMonkeyEngine (issue #1995) exactly as specified by the provided file contents. Constraints that must be preserved:

Tests

8 approved test command(s) completed successfully in the sandbox

This checklist is maintained automatically by Jaime.

Result

Adds support for the Android Game-Mode API to jme3-android, so applications can bind their own logic to the game mode selected by the user (Android 12+).

New public API (com.jme3.system.android)

  • GameMode — enum mirroring the platform constants: UNSUPPORTED (0), STANDARD (1), PERFORMANCE (2), BATTERY (3), plus isSupported(), getValue() and fromValue(int).
  • OnGameModeChangedvoid onGameModeChanged(GameMode gameMode). This is the single-listener equivalent of the per-mode callbacks described in the issue: PERFORMANCE ~ onPerformanceEnabled, BATTERY ~ onBatterySaverEnabled, STANDARD ~ onStandardEnabled, UNSUPPORTED ~ onDisabled.
  • AndroidGameMode — reflection-based bridge to android.app.GameManager: reads the current mode, registers/unregisters a GameManager.OnGameModeChangedListener proxy and re-reads the mode when a listener is registered, so the listener is always notified once with the current mode (including UNSUPPORTED on Android 11 and older, or when the platform does not report a game mode). Callbacks are dispatched on the Android main thread.

Wiring

  • JmeSurfaceView#setOnGameModeChanged(OnGameModeChanged) (the class named in the issue), registered lazily from the view context and unregistered in destroy().
  • AndroidHarnessFragment#setOnGameModeChanged(OnGameModeChanged) (the non-deprecated harness): the context is captured in onAttach, the listener is registered immediately when the fragment is already attached, otherwise at the end of onCreate, and unregistered in onDestroy.

Backward compatibility

No android.app.GameManager, Build.VERSION_CODES.S or other API-31 symbol is referenced at compile time or in any public signature; all platform access goes through java.lang.reflect and every failure degrades gracefully (logged, mode reported as UNSUPPORTED). Devices older than Android 12 keep working unchanged.

Example

jme3-android-examples: new self-contained TestGameModeActivity (declared in AndroidManifest.xml) that registers the listener on a JmeSurfaceView and prints/logs every game mode change, with comments on the kind of logic a game may apply:

adb shell am start -n org.jmonkeyengine.jme3androidexamples/.TestGameModeActivity
adb logcat -s TestGameModeActivity

Validation

The sandbox could not provision the Gradle wrapper distribution nor a pre-populated Gradle cache (Could not create parent directory for lock file ... gradle-9.4.1-bin.zip.lck), so ./gradlew could not run here; CI is the gate for the Gradle build. Validation performed instead with plain javac:

  • all jme3-core sources compiled (src/main/java, src/plugins/java, src/tools/java);
  • all jme3-android/src/main/java sources compiled against lib/android.jar (with the in-tree androidx stubs plus minimal annotation/lifecycle stubs);
  • TestGameModeActivity compiled against the freshly built jme3-core and jme3-android classes.

Two real compilation problems were found and fixed during this validation: the in-tree androidx.fragment.app.Fragment stub has no getContext() (the harness now uses the onAttach context), and the example no longer depends on MainActivity/R.

Fixes #1995.

Adds support for the Android Game-Mode API to `jme3-android`, so applications can bind their own logic to the game mode selected by the user (Android 12+).

### New public API (`com.jme3.system.android`)

* `GameMode` — enum mirroring the platform constants: `UNSUPPORTED` (0), `STANDARD` (1), `PERFORMANCE` (2), `BATTERY` (3), plus `isSupported()`, `getValue()` and `fromValue(int)`.
* `OnGameModeChanged` — `void onGameModeChanged(GameMode gameMode)`. This is the single-listener equivalent of the per-mode callbacks described in the issue: `PERFORMANCE` ~ `onPerformanceEnabled`, `BATTERY` ~ `onBatterySaverEnabled`, `STANDARD` ~ `onStandardEnabled`, `UNSUPPORTED` ~ `onDisabled`.
* `AndroidGameMode` — reflection-based bridge to `android.app.GameManager`: reads the current mode, registers/unregisters a `GameManager.OnGameModeChangedListener` proxy and re-reads the mode when a listener is registered, so the listener is always notified once with the current mode (including `UNSUPPORTED` on Android 11 and older, or when the platform does not report a game mode). Callbacks are dispatched on the Android main thread.

### Wiring

* `JmeSurfaceView#setOnGameModeChanged(OnGameModeChanged)` (the class named in the issue), registered lazily from the view context and unregistered in `destroy()`.
* `AndroidHarnessFragment#setOnGameModeChanged(OnGameModeChanged)` (the non-deprecated harness): the context is captured in `onAttach`, the listener is registered immediately when the fragment is already attached, otherwise at the end of `onCreate`, and unregistered in `onDestroy`.

### Backward compatibility

No `android.app.GameManager`, `Build.VERSION_CODES.S` or other API-31 symbol is referenced at compile time or in any public signature; all platform access goes through `java.lang.reflect` and every failure degrades gracefully (logged, mode reported as `UNSUPPORTED`). Devices older than Android 12 keep working unchanged.

### Example

`jme3-android-examples`: new self-contained `TestGameModeActivity` (declared in `AndroidManifest.xml`) that registers the listener on a `JmeSurfaceView` and prints/logs every game mode change, with comments on the kind of logic a game may apply:

```
adb shell am start -n org.jmonkeyengine.jme3androidexamples/.TestGameModeActivity
adb logcat -s TestGameModeActivity
```

### Validation

The sandbox could not provision the Gradle wrapper distribution nor a pre-populated Gradle cache (`Could not create parent directory for lock file ... gradle-9.4.1-bin.zip.lck`), so `./gradlew` could not run here; CI is the gate for the Gradle build. Validation performed instead with plain javac:

* all `jme3-core` sources compiled (`src/main/java`, `src/plugins/java`, `src/tools/java`);
* all `jme3-android/src/main/java` sources compiled against `lib/android.jar` (with the in-tree androidx stubs plus minimal annotation/lifecycle stubs);
* `TestGameModeActivity` compiled against the freshly built `jme3-core` and `jme3-android` classes.

Two real compilation problems were found and fixed during this validation: the in-tree `androidx.fragment.app.Fragment` stub has no `getContext()` (the harness now uses the `onAttach` context), and the example no longer depends on `MainActivity`/`R`.

Fixes jMonkeyEngine#1995.
@jaime-jmebot jaime-jmebot changed the title Add Android Game-Mode API listener support Add Android Game-Mode API listener support (#1995) Sep 17, 2026
@jaime-jmebot
jaime-jmebot marked this pull request as ready for review September 17, 2026 17:27

@riccardobl riccardobl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR has some problems, please check the review.
Also, please, implement GameManager.setGameState() available since API 33.

* @see GameMode
* @see OnGameModeChanged
*/
public class AndroidGameMode {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use reflection, please use if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ) { and access access GameManager directly.
Increase compile sdk if needed.

This won't break android 11 as long as the new API calls are guarded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the reflection is unnecessary and is in fact what breaks the feature (see the listener thread below). The bridge will call GameManager directly, guarded per member by the level it was added: getGameMode() / GAME_MODE_* = API 31 (S), setGameState(...) = API 33 (TIRAMISU), GAME_MODE_CUSTOM = API 34 (UPSIDE_DOWN_CAKE).

Two build consequences to settle with it: jme3-android has no AGP compileSdk (see jme3-android/build.gradle, it compiles against the checked-in lib/android.jar), so that jar has to be bumped to API 34; and the GameManager instance still needs a null check, since the platform does not guarantee an instance on all devices (Wear devices may not publish one).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked on the compile SDK, so no commit yet — I did not want to push code that cannot build.

The reflection-free bridge is written (API 31 guard for getGameMode()/GAME_MODE_*, API 33 guard for setGameState(GameState), no listener proxy, mode read on resume, CUSTOM(4)), but jme3-android has no AGP compileSdk: it compiles against the checked-in lib/android.jar (jme3-android/build.gradle, compileOnly), and the CI build job for the engine has no Android SDK, so that jar is the only source of android.* symbols in the build.

That jar is too old for this code. Verified in the sandbox on this checkout:

-rw-r--r-- 1 root root 46784255 lib/android.jar
$ javap -classpath lib/android.jar android.app.GameManager
Error: class not found: android.app.GameManager
$ javap -classpath lib/android.jar 'android.os.Build$VERSION_CODES' | grep -E ' (S|TIRAMISU|UPSIDE_DOWN_CAKE) = '
(no output)

So javac of the bridge against this jar fails, and "increase compile sdk" here means replacing lib/android.jar with an android-34 jar — a ~47 MB binary, which my patch path (text files only) cannot write.

Question: do you want to land the jar bump (or resolve #1148 so the jar comes from the local SDK) and then I push the reflection-free version? In the meantime I can land an interim fix that keeps reflection but reads GameManager.getGameMode() on resume and adds CUSTOM(4) — say the word and I'll push that instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, go ahead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Going with the reflection-free version, queued as an action item. Scope it locks in:

  • android.app.GameManager used directly, guarded per member: getGameMode() / GAME_MODE_* at S, setGameState(GameState) at TIRAMISU, GAME_MODE_CUSTOM at UPSIDE_DOWN_CAKE.
  • Listener proxy, register()/unregister() and the re-read-on-registration dropped; the mode is read on resume (JmeSurfaceView on ON_RESUME, AndroidHarnessFragment.onResume()) and pushed to the jME listener.
  • GameMode.CUSTOM(4); fromValue() keeps mapping unknown values to UNSUPPORTED.
  • Null-safe GameManager lookup, and android:isGame="true" for the example app so the platform actually reports a mode.

One open point I cannot settle myself: jme3-android compiles against the checked-in lib/android.jar (compileOnly in jme3-android/build.gradle), and that jar has neither android.app.GameManager nor VERSION_CODES.S/TIRAMISU/UPSIDE_DOWN_CAKE, so the reflection-free sources do not compile against it. It is a ~47 MB binary, which I cannot write through my patch path. Two ways out:

  1. you land an android-34 lib/android.jar (or resolve android.jar should be loaded from the local android sdk installation and not shipped with the engine. #1148 so the jar comes from the SDK), or
  2. I add the required API-31/33/34 signatures as a compile-only stub source set in jme3-android, following the existing src/androidx-stubs/java precedent in the same module.

The Java sources are identical either way. Which do you prefer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve issue #1148

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted — #1148 it is, queued as an action item on this branch. What I can write here (text files only):

  • jme3-android/build.gradle: drop files(rootProject.file('lib/android.jar')) and compile against the platform jar of the locally installed SDK, reusing the discovery already in settings.gradle (findAndroidSdk, androidExamplesMinCompileSdk = 34), failing with a clear message when no API ≥ 34 platform is installed. The same minimum the example app already requires.
  • The reflection-free AndroidGameMode / GameMode sources exactly as scoped above (per-member S/TIRAMISU/UPSIDE_DOWN_CAKE guards, no listener proxy, mode read on resume, CUSTOM(4), setGameState(GameState) from the accepted review).

Two parts of #1148 I cannot complete from here, and they are not optional:

  1. lib/android.jar is a checked-in ~47 MB binary. My patch path writes text only, so I cannot delete it — a maintainer (or the host) has to remove it in a commit. Tell me if you also want the path added to .gitignore.
  2. jme3-android is included unconditionally in settings.gradle and is built by every engine job, and none of them install an Android SDK: BuildJMonkey (ubuntu/windows/macOS) runs only setup-java + build createZipDistribution mergedJavadoc, and JavadocDoclint/SpotBugs compile the same module. With SDK-only resolution those jobs fail until platforms;android-34 is provisioned (e.g. android-actions/setup-android in .github/workflows/main.yml). I can write that file too, but say whether you want the CI provisioning inside this game-mode PR or as a separate one — I don't want to fold a repo-wide build-infra change into it silently.

Either way ./gradlew is unverifiable in my sandbox (no Android SDK, no Gradle distribution), so the first push may land red and CI is the real check. If you prefer, land #1148 on its own and I rebase the GameMode sources on top; the Java sources are identical either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Landed GameMode.CUSTOM(4) as b18fb0a (branch jaime/issue-1995-6df48d20), with fromValue() still mapping unknown values to UNSUPPORTED and the docs updated. That is the one review item that does not depend on the compile classpath, so it was the only part of "go ahead" I could push without breaking the build. Sandbox checks pass (enum compiles; 0→UNSUPPORTED, 1/2/3 unchanged, 4→CUSTOM, 5/-1→UNSUPPORTED, CUSTOM.getValue()==4, CUSTOM.isSupported(), !UNSUPPORTED.isSupported()).

The reflection-free bridge is written but still unpushable, and the compile SDK is now the only thing in the way. In this module "increase compile sdk" means replacing the checked-in lib/android.jar (it is compileOnly in jme3-android/build.gradle, and CI's BuildJMonkey job has no Android SDK, so that jar is the only source of android.* symbols). On this checkout the jar predates API 31: it has neither android.app.GameManager nor Build.VERSION_CODES.S, so the sources cannot compile against it.

The alternative I offered earlier (a stub source set) does not actually cover the guard you asked for: android.os.Build$VERSION_CODES already exists in that jar, so it cannot be extended with a stub — stubbing only GameManager/GameState would force literal 31/33/34 instead of VERSION_CODES.S/TIRAMISU/UPSIDE_DOWN_CAKE, and would put a fake android.app.GameManager on the engine's compile classpath. I would rather not land that.

So the ask: can you land an API 34 lib/android.jar (or resolve #1148 so the jar comes from the SDK)? As soon as the classpath has API 34 I push the rewrite in one commit — direct GameManager use guarded per member (S / TIRAMISU / UPSIDE_DOWN_CAKE), mode read on ON_RESUME / onResume() and pushed to the jME listener, no listener proxy and no register/unregister, null-safe manager lookup, setGameState(GameState), and android:isGame="true" for the example app.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go ahead and fix the ci too, don't worry about removing the jar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted — CI provisioning comes into this PR, and the checked-in lib/android.jar stays in the tree (it simply stops being referenced).

Queued for the write phase:

  • jme3-android/build.gradle: drop compileOnly files(rootProject.file('lib/android.jar')) (both configurations) and use the platform android.jar of the locally installed SDK, reusing the discovery already in settings.gradle (findAndroidSdk, androidExamplesMinCompileSdk = 34), with an explicit failure message when no API ≥ 34 platform is installed.
  • .github/workflows/main.yml: provision platforms;android-34 in every job that configures or compiles jme3-androidCheckstyle, SpotBugs, JavadocDoclint, ScreenshotTests, BuildJMonkey (ubuntu/windows/macOS), and DeployJavaSnapshot / DeployRelease (both run a Gradle build on ubuntu-latest), since the module is included unconditionally and those jobs currently have no Android SDK.
  • The reflection-free AndroidGameMode / GameMode rewrite exactly as scoped earlier: direct GameManager use guarded per member (S for getGameMode()/GAME_MODE_*, TIRAMISU for setGameState(GameState), UPSIDE_DOWN_CAKE for GAME_MODE_CUSTOM), no listener proxy and no register/unregister, mode read on ON_RESUME / onResume() and pushed to the jME listener, null-safe manager lookup, android:isGame="true" in the example manifest.

./gradlew still cannot run here (no Gradle distribution, no Android SDK), so the first push may land red and CI is the real gate on both the classpath change and the rewrite. Write phase requested.

public Object invoke(Object proxy, Method method, Object[] args) {
if (GAME_MODE_CHANGED_METHOD_NAME.equals(method.getName())
&& args != null && args.length == 1 && args[0] instanceof Integer) {
dispatch(GameMode.fromValue((Integer) args[0]));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nonsense, just use use GameManager.getGameMode() on resume and notify the jme listener. Check android documentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and the impact is bigger than this call site: the platform has no game-mode change listener at all. android.app.GameManager in android-12.0.0_r1 (API 31), android-13.0.0_r1 (API 33), android-14.0.0_r1 (API 34) and current AOSP master declares only getGameMode() (and setGameState() from API 33) — neither GameManager$OnGameModeChangedListener nor registerGameModeChangedListener() exists, and the getGameMode() javadoc says "Developers should call this API every time the application is resumed".

That also means the current code cannot work on any device: the constructor nulls the whole bridge when Class.forName("android.app.GameManager$OnGameModeChangedListener") fails (same try block as the manager lookup), so getGameMode() is never invoked and the API reports UNSUPPORTED even on Android 12+.

The rewrite drops the proxy, register(), unregister() and the re-read-on-registration, and instead reads GameManager.getGameMode() on resume (JmeSurfaceView on ON_RESUME, AndroidHarnessFragment.onResume()) and notifies the listener with that value.

* saver mode for the game.
*/
BATTERY(3);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Api 34 supports CUSTOM as 4th value. Verify with android docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. GameManager.GAME_MODE_CUSTOM = 4 was added in API 34 (UPSIDE_DOWN_CAKE) and is still the highest value in AOSP master (android-14.0.0_r1 framework source). CUSTOM(4) will be added to the enum, and fromValue() keeps mapping unknown values to UNSUPPORTED. Note the platform only reports CUSTOM for targetSdk ≥ 34, otherwise getGameMode() returns STANDARD for it.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All review items are accepted and tracked as action items for the rewrite; nothing is claimable as fixed yet.

  • Reflection removed: GameManager is used directly, guarded per member (S for getGameMode()/GAME_MODE_*, TIRAMISU for setGameState(...), UPSIDE_DOWN_CAKE for GAME_MODE_CUSTOM). jme3-android compiles against the checked-in lib/android.jar, so that jar must move to API 34.
  • The listener cannot be implemented as specified: no game-mode-changed listener exists in the platform API (API 31/33/34 and master), and the current Class.forName coupling makes the bridge always report UNSUPPORTED. Replaced by reading GameManager.getGameMode() on resume and notifying the listener there.
  • GameMode gains CUSTOM(4) (API 34); fromValue() still maps unknown values to UNSUPPORTED.
  • setGameState() (API 33) will be added, taking android.app.GameState — the released signature is setGameState(GameState), not a boolean.
  • Example: TestGameModeActivity is wired, but the example app is not declared as a game in AndroidManifest.xml, so the platform always reports UNSUPPORTED for it; the manifest needs android:isGame="true".

@jaime-jmebot
jaime-jmebot marked this pull request as draft September 17, 2026 18:00

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied in the thread and requested the write phase for the accepted changes.

  • Direction accepted: resolve #1148 — replace the checked-in lib/android.jar with the locally installed SDK platform jar (≥ API 34, reusing settings.gradle's discovery) and rewrite the bridge reflection-free.
  • Flagged the two parts outside my reach: deleting the ~47 MB binary jar, and provisioning platforms;android-34 in the engine CI jobs (BuildJMonkey, JavadocDoclint, SpotBugs), which otherwise fail since jme3-android is built by every job without an Android SDK.
  • Asked the one open scope question: CI provisioning inside this PR or separately.

@jaime-jmebot
jaime-jmebot marked this pull request as ready for review September 17, 2026 18:12

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied in the thread confirming the accepted scope (CI provisioning inside this PR, jar left untouched) and requested the write phase for the build/CI/rewrite changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Add support for the new Game-Mode API

2 participants