From c04e120349c9720d4c1c2883fe4bbf72981a2253 Mon Sep 17 00:00:00 2001
From: Davis Rollman
Date: Sun, 10 May 2026 17:51:52 -0700
Subject: [PATCH 1/5] Restore deprecated AndroidHarness
---
jme3-android/build.gradle | 11 +-
.../java/com/jme3/app/AndroidHarness.java | 284 ++++++++++++++++++
2 files changed, 287 insertions(+), 8 deletions(-)
create mode 100644 jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
diff --git a/jme3-android/build.gradle b/jme3-android/build.gradle
index 2bf4908a10..1fcabf376c 100644
--- a/jme3-android/build.gradle
+++ b/jme3-android/build.gradle
@@ -1,11 +1,3 @@
-sourceSets {
- main {
- java {
- srcDir 'src/androidx-stubs/java'
- }
- }
-}
-
dependencies {
//added annotations used by JmeSurfaceView.
compileOnly libs.androidx.annotation
@@ -17,6 +9,8 @@ dependencies {
compileJava {
// The Android-Native Project requires the jni headers to be generated, so we do that here
options.compilerArgs += ["-h", "${project.rootDir}/jme3-android-native/src/native/headers"]
+ options.sourcepath = files("src/androidx-stubs/java")
+ options.compilerArgs += ["-implicit:none"]
}
tasks.withType(Jar).configureEach {
@@ -28,5 +22,6 @@ tasks.named('sourcesJar') {
}
javadoc {
+ source += fileTree("src/androidx-stubs/java")
exclude('androidx/**')
}
diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
new file mode 100644
index 0000000000..9e96102989
--- /dev/null
+++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2009-2026 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.app;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import androidx.fragment.app.FragmentActivity;
+import com.jme3.input.TouchInput;
+import com.jme3.input.controls.TouchListener;
+import com.jme3.input.controls.TouchTrigger;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.system.AppSettings;
+import java.lang.reflect.Method;
+import java.util.logging.Logger;
+
+/**
+ * Legacy Activity wrapper for running a jME application on Android.
+ *
+ * @deprecated Use {@link AndroidHarnessFragment} from an AndroidX
+ * {@link FragmentActivity} instead.
+ */
+@Deprecated
+public class AndroidHarness extends FragmentActivity {
+
+ protected static final Logger logger = Logger.getLogger(AndroidHarness.class.getName());
+
+ /**
+ * The application class to start.
+ */
+ protected String appClass = "jme3test.android.Test";
+
+ /**
+ * The jME application object.
+ */
+ protected LegacyApplication app;
+
+ protected int eglBitsPerPixel = 24;
+ protected int eglAlphaBits = 0;
+ protected int eglDepthBits = 16;
+ protected int eglSamples = 0;
+ protected int eglStencilBits = 0;
+ protected int frameRate = -1;
+ protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
+ protected boolean joystickEventsEnabled = false;
+ protected boolean keyEventsEnabled = true;
+ protected boolean mouseEventsEnabled = true;
+ protected boolean mouseEventsInvertX = false;
+ protected boolean mouseEventsInvertY = false;
+ protected boolean finishOnAppStop = true;
+ protected boolean handleExitHook = true;
+ protected String exitDialogTitle = "Do you want to exit?";
+ protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it.";
+ protected boolean screenFullScreen = true;
+ protected boolean screenShowTitle = true;
+ protected int splashPicID = 0;
+
+ private HarnessFragment fragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ configureWindow();
+
+ fragment = new HarnessFragment();
+ fragment.setFinishOnAppStop(finishOnAppStop);
+ attachFragment(fragment);
+ }
+
+ private void configureWindow() {
+ if (screenFullScreen) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ } else if (!screenShowTitle) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ }
+
+ public Application getJmeApplication() {
+ return app;
+ }
+
+ private void attachFragment(AndroidHarnessFragment fragment) {
+ try {
+ Method getSupportFragmentManager = getClass().getMethod("getSupportFragmentManager");
+ Object fragmentManager = getSupportFragmentManager.invoke(this);
+ Object transaction = fragmentManager.getClass().getMethod("beginTransaction").invoke(fragmentManager);
+ transaction = transaction.getClass()
+ .getMethod("replace", int.class, androidx.fragment.app.Fragment.class)
+ .invoke(transaction, android.R.id.content, fragment);
+ transaction.getClass().getMethod("commit").invoke(transaction);
+ } catch (ReflectiveOperationException exception) {
+ throw new IllegalStateException("Unable to attach AndroidHarnessFragment", exception);
+ }
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ if (app != null) {
+ app.restart();
+ }
+ }
+
+ private class HarnessFragment extends AndroidHarnessFragment
+ implements TouchListener, DialogInterface.OnClickListener {
+
+ private static final String ESCAPE_EVENT = "TouchEscape";
+
+ private FrameLayout frameLayout;
+ private ImageView splashImageView;
+ private boolean firstDrawFrame = true;
+
+ @Override
+ protected LegacyApplication createApplication() throws Exception {
+ Class> clazz = Class.forName(appClass);
+ app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
+ return app;
+ }
+
+ @Override
+ protected AppSettings createSettings() {
+ AppSettings settings = super.createSettings();
+ settings.setAudioRenderer(audioRendererType);
+ return settings;
+ }
+
+ @Override
+ protected void configureSettings(AppSettings settings) {
+ settings.setEmulateMouse(mouseEventsEnabled);
+ settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);
+ settings.setUseJoysticks(joystickEventsEnabled);
+ settings.setEmulateKeyboard(keyEventsEnabled);
+
+ settings.setBitsPerPixel(eglBitsPerPixel);
+ settings.setAlphaBits(eglAlphaBits);
+ settings.setDepthBits(eglDepthBits);
+ settings.setSamples(eglSamples);
+ settings.setStencilBits(eglStencilBits);
+ settings.setFrameRate(frameRate);
+ }
+
+ @Override
+ public View onCreateView(android.view.LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ View jmeView = super.onCreateView(inflater, container, savedInstanceState);
+ if (splashPicID == 0 || app == null) {
+ return jmeView;
+ }
+
+ frameLayout = new FrameLayout(AndroidHarness.this);
+ frameLayout.addView(jmeView);
+
+ splashImageView = new ImageView(AndroidHarness.this);
+ Drawable drawable = getResources().getDrawable(splashPicID);
+ if (drawable instanceof NinePatchDrawable) {
+ splashImageView.setBackgroundDrawable(drawable);
+ } else {
+ splashImageView.setImageResource(splashPicID);
+ }
+
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ Gravity.CENTER);
+ frameLayout.addView(splashImageView, layoutParams);
+ return frameLayout;
+ }
+
+ @Override
+ public void onDestroyView() {
+ if (splashImageView != null && splashImageView.getParent() instanceof ViewGroup) {
+ ((ViewGroup) splashImageView.getParent()).removeView(splashImageView);
+ }
+ if (frameLayout != null && frameLayout.getParent() instanceof ViewGroup) {
+ ((ViewGroup) frameLayout.getParent()).removeView(frameLayout);
+ }
+ splashImageView = null;
+ frameLayout = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ if (handleExitHook) {
+ if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
+ app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
+ }
+ app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
+ app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
+ }
+ }
+
+ @Override
+ public void update() {
+ super.update();
+ if (firstDrawFrame) {
+ removeSplashScreen();
+ firstDrawFrame = false;
+ }
+ }
+
+ @Override
+ public void onTouch(String name, TouchEvent event, float tpf) {
+ if (ESCAPE_EVENT.equals(name) && event.getType() == TouchEvent.Type.KEY_UP) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(AndroidHarness.this)
+ .setTitle(exitDialogTitle)
+ .setMessage(exitDialogMessage)
+ .setPositiveButton("Yes", HarnessFragment.this)
+ .setNegativeButton("No", HarnessFragment.this)
+ .create()
+ .show();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (whichButton != DialogInterface.BUTTON_NEGATIVE) {
+ if (app != null) {
+ app.stop(true);
+ }
+ app = null;
+ finish();
+ }
+ }
+
+ private void removeSplashScreen() {
+ if (splashImageView != null && frameLayout != null) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ splashImageView.setVisibility(View.INVISIBLE);
+ frameLayout.removeView(splashImageView);
+ }
+ });
+ }
+ }
+ }
+}
From 80db3e35032df9eab09b659852d831bfc0c6a544 Mon Sep 17 00:00:00 2001
From: Davis Rollman
Date: Thu, 14 May 2026 19:32:20 -0700
Subject: [PATCH 2/5] Make AndroidHarness fragment recreateable
---
.../java/com/jme3/app/AndroidHarness.java | 78 +++++++++++--------
1 file changed, 44 insertions(+), 34 deletions(-)
diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
index 9e96102989..eb4462f9ec 100644
--- a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
+++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
@@ -141,7 +141,7 @@ protected void onRestart() {
}
}
- private class HarnessFragment extends AndroidHarnessFragment
+ public static class HarnessFragment extends AndroidHarnessFragment
implements TouchListener, DialogInterface.OnClickListener {
private static final String ESCAPE_EVENT = "TouchEscape";
@@ -150,52 +150,59 @@ private class HarnessFragment extends AndroidHarnessFragment
private ImageView splashImageView;
private boolean firstDrawFrame = true;
+ private AndroidHarness harness() {
+ return (AndroidHarness) requireActivity();
+ }
+
@Override
protected LegacyApplication createApplication() throws Exception {
- Class> clazz = Class.forName(appClass);
- app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
- return app;
+ AndroidHarness harness = harness();
+ Class> clazz = Class.forName(harness.appClass);
+ harness.app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
+ return harness.app;
}
@Override
protected AppSettings createSettings() {
AppSettings settings = super.createSettings();
- settings.setAudioRenderer(audioRendererType);
+ settings.setAudioRenderer(harness().audioRendererType);
return settings;
}
@Override
protected void configureSettings(AppSettings settings) {
- settings.setEmulateMouse(mouseEventsEnabled);
- settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);
- settings.setUseJoysticks(joystickEventsEnabled);
- settings.setEmulateKeyboard(keyEventsEnabled);
+ AndroidHarness harness = harness();
+ settings.setEmulateMouse(harness.mouseEventsEnabled);
+ settings.setEmulateMouseFlipAxis(harness.mouseEventsInvertX, harness.mouseEventsInvertY);
+ settings.setUseJoysticks(harness.joystickEventsEnabled);
+ settings.setEmulateKeyboard(harness.keyEventsEnabled);
- settings.setBitsPerPixel(eglBitsPerPixel);
- settings.setAlphaBits(eglAlphaBits);
- settings.setDepthBits(eglDepthBits);
- settings.setSamples(eglSamples);
- settings.setStencilBits(eglStencilBits);
- settings.setFrameRate(frameRate);
+ settings.setBitsPerPixel(harness.eglBitsPerPixel);
+ settings.setAlphaBits(harness.eglAlphaBits);
+ settings.setDepthBits(harness.eglDepthBits);
+ settings.setSamples(harness.eglSamples);
+ settings.setStencilBits(harness.eglStencilBits);
+ settings.setFrameRate(harness.frameRate);
}
@Override
public View onCreateView(android.view.LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View jmeView = super.onCreateView(inflater, container, savedInstanceState);
- if (splashPicID == 0 || app == null) {
+ AndroidHarness harness = harness();
+ if (harness.splashPicID == 0 || harness.app == null) {
return jmeView;
}
- frameLayout = new FrameLayout(AndroidHarness.this);
+ frameLayout = new FrameLayout(harness);
frameLayout.addView(jmeView);
- splashImageView = new ImageView(AndroidHarness.this);
- Drawable drawable = getResources().getDrawable(splashPicID);
+ splashImageView = new ImageView(harness);
+ Drawable drawable = getResources().getDrawable(harness.splashPicID);
if (drawable instanceof NinePatchDrawable) {
splashImageView.setBackgroundDrawable(drawable);
} else {
- splashImageView.setImageResource(splashPicID);
+ splashImageView.setImageResource(harness.splashPicID);
}
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
@@ -222,12 +229,13 @@ public void onDestroyView() {
@Override
public void initialize() {
super.initialize();
- if (handleExitHook) {
- if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
- app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
+ AndroidHarness harness = harness();
+ if (harness.handleExitHook) {
+ if (harness.app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
+ harness.app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
}
- app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
- app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
+ harness.app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
+ harness.app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
}
}
@@ -243,12 +251,13 @@ public void update() {
@Override
public void onTouch(String name, TouchEvent event, float tpf) {
if (ESCAPE_EVENT.equals(name) && event.getType() == TouchEvent.Type.KEY_UP) {
- runOnUiThread(new Runnable() {
+ harness().runOnUiThread(new Runnable() {
@Override
public void run() {
- new AlertDialog.Builder(AndroidHarness.this)
- .setTitle(exitDialogTitle)
- .setMessage(exitDialogMessage)
+ AndroidHarness harness = harness();
+ new AlertDialog.Builder(harness)
+ .setTitle(harness.exitDialogTitle)
+ .setMessage(harness.exitDialogMessage)
.setPositiveButton("Yes", HarnessFragment.this)
.setNegativeButton("No", HarnessFragment.this)
.create()
@@ -261,17 +270,18 @@ public void run() {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
if (whichButton != DialogInterface.BUTTON_NEGATIVE) {
- if (app != null) {
- app.stop(true);
+ AndroidHarness harness = harness();
+ if (harness.app != null) {
+ harness.app.stop(true);
}
- app = null;
- finish();
+ harness.app = null;
+ harness.finish();
}
}
private void removeSplashScreen() {
if (splashImageView != null && frameLayout != null) {
- runOnUiThread(new Runnable() {
+ harness().runOnUiThread(new Runnable() {
@Override
public void run() {
splashImageView.setVisibility(View.INVISIBLE);
From 37d0f8d5a51a0a58112c7c5192c2a13b5a2ef9c4 Mon Sep 17 00:00:00 2001
From: Davis Rollman
Date: Thu, 14 May 2026 22:35:05 -0700
Subject: [PATCH 3/5] Restore AndroidHarness compatibility hooks
---
.../java/com/jme3/app/AndroidHarness.java | 347 ++++++++++++++----
1 file changed, 270 insertions(+), 77 deletions(-)
diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
index eb4462f9ec..7595fb952e 100644
--- a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
+++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
@@ -35,6 +35,7 @@
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
+import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
@@ -44,12 +45,21 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.fragment.app.FragmentActivity;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.input.JoyInput;
import com.jme3.input.TouchInput;
+import com.jme3.input.android.AndroidSensorJoyInput;
import com.jme3.input.controls.TouchListener;
import com.jme3.input.controls.TouchTrigger;
import com.jme3.input.event.TouchEvent;
import com.jme3.system.AppSettings;
+import com.jme3.system.SystemListener;
+import com.jme3.system.android.JmeAndroidSystem;
+import com.jme3.system.android.OGLESContext;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.lang.reflect.Method;
+import java.util.logging.Level;
import java.util.logging.Logger;
/**
@@ -59,9 +69,12 @@
* {@link FragmentActivity} instead.
*/
@Deprecated
-public class AndroidHarness extends FragmentActivity {
+public class AndroidHarness extends FragmentActivity
+ implements TouchListener, DialogInterface.OnClickListener, SystemListener {
protected static final Logger logger = Logger.getLogger(AndroidHarness.class.getName());
+ private static final String HARNESS_FRAGMENT_TAG = "com.jme3.app.AndroidHarness.fragment";
+ private static final String ESCAPE_EVENT = "TouchEscape";
/**
* The application class to start.
@@ -93,16 +106,22 @@ public class AndroidHarness extends FragmentActivity {
protected boolean screenShowTitle = true;
protected int splashPicID = 0;
+ protected OGLESContext ctx;
+ protected GLSurfaceView view;
+ protected boolean isGLThreadPaused = true;
+ protected ImageView splashImageView;
+ protected FrameLayout frameLayout;
+
private HarnessFragment fragment;
+ private boolean firstDrawFrame = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureWindow();
- fragment = new HarnessFragment();
+ fragment = attachFragment();
fragment.setFinishOnAppStop(finishOnAppStop);
- attachFragment(fragment);
}
private void configureWindow() {
@@ -119,15 +138,24 @@ public Application getJmeApplication() {
return app;
}
- private void attachFragment(AndroidHarnessFragment fragment) {
+ private HarnessFragment attachFragment() {
try {
Method getSupportFragmentManager = getClass().getMethod("getSupportFragmentManager");
Object fragmentManager = getSupportFragmentManager.invoke(this);
+ Object existingFragment = fragmentManager.getClass()
+ .getMethod("findFragmentByTag", String.class)
+ .invoke(fragmentManager, HARNESS_FRAGMENT_TAG);
+ if (existingFragment instanceof HarnessFragment) {
+ return (HarnessFragment) existingFragment;
+ }
+
+ HarnessFragment newFragment = new HarnessFragment();
Object transaction = fragmentManager.getClass().getMethod("beginTransaction").invoke(fragmentManager);
transaction = transaction.getClass()
- .getMethod("replace", int.class, androidx.fragment.app.Fragment.class)
- .invoke(transaction, android.R.id.content, fragment);
+ .getMethod("replace", int.class, androidx.fragment.app.Fragment.class, String.class)
+ .invoke(transaction, android.R.id.content, newFragment, HARNESS_FRAGMENT_TAG);
transaction.getClass().getMethod("commit").invoke(transaction);
+ return newFragment;
} catch (ReflectiveOperationException exception) {
throw new IllegalStateException("Unable to attach AndroidHarnessFragment", exception);
}
@@ -141,14 +169,203 @@ protected void onRestart() {
}
}
- public static class HarnessFragment extends AndroidHarnessFragment
- implements TouchListener, DialogInterface.OnClickListener {
+ @Override
+ public void handleError(final String errorMsg, final Throwable throwable) {
+ String stackTrace = "";
+ String title = "Error";
+
+ if (throwable != null) {
+ StringWriter writer = new StringWriter(100);
+ throwable.printStackTrace(new PrintWriter(writer));
+ stackTrace = writer.toString();
+ title = throwable.toString();
+ }
+
+ final String finalTitle = title;
+ final String finalMessage = (errorMsg != null ? errorMsg : "Uncaught Exception")
+ + "\n" + stackTrace;
+
+ logger.log(Level.SEVERE, finalMessage);
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(AndroidHarness.this)
+ .setTitle(finalTitle)
+ .setMessage(finalMessage)
+ .setPositiveButton("Kill", AndroidHarness.this)
+ .create()
+ .show();
+ }
+ });
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (whichButton != DialogInterface.BUTTON_NEGATIVE) {
+ if (app != null) {
+ app.stop(true);
+ }
+ app = null;
+ finish();
+ }
+ }
+
+ @Override
+ public void onTouch(String name, TouchEvent event, float tpf) {
+ if (ESCAPE_EVENT.equals(name) && event.getType() == TouchEvent.Type.KEY_UP) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(AndroidHarness.this)
+ .setTitle(exitDialogTitle)
+ .setMessage(exitDialogMessage)
+ .setPositiveButton("Yes", AndroidHarness.this)
+ .setNegativeButton("No", AndroidHarness.this)
+ .create()
+ .show();
+ }
+ });
+ }
+ }
+
+ public void layoutDisplay() {
+ logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID);
+ frameLayout = null;
+ splashImageView = null;
+
+ if (splashPicID == 0 || view == null) {
+ return;
+ }
+
+ frameLayout = new FrameLayout(this);
+ frameLayout.addView(view);
+
+ splashImageView = new ImageView(this);
+ Drawable drawable = getResources().getDrawable(splashPicID);
+ if (drawable instanceof NinePatchDrawable) {
+ splashImageView.setBackgroundDrawable(drawable);
+ } else {
+ splashImageView.setImageResource(splashPicID);
+ }
+
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ Gravity.CENTER);
+ frameLayout.addView(splashImageView, layoutParams);
+ }
+
+ public void removeSplashScreen() {
+ if (splashImageView != null && frameLayout != null) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ splashImageView.setVisibility(View.INVISIBLE);
+ frameLayout.removeView(splashImageView);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void initialize() {
+ app.initialize();
+ if (handleExitHook) {
+ if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
+ app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
+ }
+ app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
+ app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
+ }
+ }
- private static final String ESCAPE_EVENT = "TouchEscape";
+ @Override
+ public void reshape(int width, int height) {
+ app.reshape(width, height);
+ }
+
+ @Override
+ public void rescale(float x, float y) {
+ app.rescale(x, y);
+ }
+
+ @Override
+ public void update() {
+ app.update();
+ if (firstDrawFrame) {
+ removeSplashScreen();
+ firstDrawFrame = false;
+ }
+ }
+
+ @Override
+ public void requestClose(boolean esc) {
+ app.requestClose(esc);
+ }
+
+ @Override
+ public void destroy() {
+ if (app != null) {
+ app.destroy();
+ }
+ if (finishOnAppStop) {
+ finish();
+ }
+ }
+
+ @Override
+ public void gainFocus() {
+ logger.fine("gainFocus");
+ if (view != null) {
+ view.onResume();
+ }
+
+ if (app != null) {
+ AudioRenderer audioRenderer = app.getAudioRenderer();
+ if (audioRenderer != null) {
+ audioRenderer.resumeAll();
+ }
+
+ JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null;
+ if (joyInput instanceof AndroidSensorJoyInput) {
+ ((AndroidSensorJoyInput) joyInput).resumeSensors();
+ }
+
+ app.gainFocus();
+ }
+ isGLThreadPaused = false;
+ }
+
+ @Override
+ public void loseFocus() {
+ logger.fine("loseFocus");
+ if (app != null) {
+ app.loseFocus();
+ }
+
+ if (view != null) {
+ view.onPause();
+ }
+
+ if (app != null) {
+ AudioRenderer audioRenderer = app.getAudioRenderer();
+ if (audioRenderer != null) {
+ audioRenderer.pauseAll();
+ }
+
+ JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null;
+ if (joyInput instanceof AndroidSensorJoyInput) {
+ ((AndroidSensorJoyInput) joyInput).pauseSensors();
+ }
+ }
+ isGLThreadPaused = true;
+ }
+
+ public static class HarnessFragment extends AndroidHarnessFragment {
private FrameLayout frameLayout;
private ImageView splashImageView;
- private boolean firstDrawFrame = true;
private AndroidHarness harness() {
return (AndroidHarness) requireActivity();
@@ -190,27 +407,18 @@ public View onCreateView(android.view.LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View jmeView = super.onCreateView(inflater, container, savedInstanceState);
AndroidHarness harness = harness();
- if (harness.splashPicID == 0 || harness.app == null) {
- return jmeView;
+ if (jmeView instanceof GLSurfaceView) {
+ harness.view = (GLSurfaceView) jmeView;
}
-
- frameLayout = new FrameLayout(harness);
- frameLayout.addView(jmeView);
-
- splashImageView = new ImageView(harness);
- Drawable drawable = getResources().getDrawable(harness.splashPicID);
- if (drawable instanceof NinePatchDrawable) {
- splashImageView.setBackgroundDrawable(drawable);
- } else {
- splashImageView.setImageResource(harness.splashPicID);
+ harness.ctx = harness.app != null ? (OGLESContext) harness.app.getContext() : null;
+ if (harness.app == null) {
+ return jmeView;
}
- FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- Gravity.CENTER);
- frameLayout.addView(splashImageView, layoutParams);
- return frameLayout;
+ harness.layoutDisplay();
+ frameLayout = harness.frameLayout;
+ splashImageView = harness.splashImageView;
+ return frameLayout != null ? frameLayout : jmeView;
}
@Override
@@ -223,72 +431,57 @@ public void onDestroyView() {
}
splashImageView = null;
frameLayout = null;
+ AndroidHarness harness = harness();
+ harness.frameLayout = null;
+ harness.splashImageView = null;
+ harness.view = null;
+ JmeAndroidSystem.setView(null);
super.onDestroyView();
}
@Override
public void initialize() {
- super.initialize();
- AndroidHarness harness = harness();
- if (harness.handleExitHook) {
- if (harness.app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
- harness.app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
- }
- harness.app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
- harness.app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
- }
+ harness().initialize();
+ }
+
+ @Override
+ public void reshape(int width, int height) {
+ harness().reshape(width, height);
+ }
+
+ @Override
+ public void rescale(float x, float y) {
+ harness().rescale(x, y);
}
@Override
public void update() {
- super.update();
- if (firstDrawFrame) {
- removeSplashScreen();
- firstDrawFrame = false;
- }
+ harness().update();
}
@Override
- public void onTouch(String name, TouchEvent event, float tpf) {
- if (ESCAPE_EVENT.equals(name) && event.getType() == TouchEvent.Type.KEY_UP) {
- harness().runOnUiThread(new Runnable() {
- @Override
- public void run() {
- AndroidHarness harness = harness();
- new AlertDialog.Builder(harness)
- .setTitle(harness.exitDialogTitle)
- .setMessage(harness.exitDialogMessage)
- .setPositiveButton("Yes", HarnessFragment.this)
- .setNegativeButton("No", HarnessFragment.this)
- .create()
- .show();
- }
- });
- }
+ public void requestClose(boolean esc) {
+ harness().requestClose(esc);
}
@Override
- public void onClick(DialogInterface dialog, int whichButton) {
- if (whichButton != DialogInterface.BUTTON_NEGATIVE) {
- AndroidHarness harness = harness();
- if (harness.app != null) {
- harness.app.stop(true);
- }
- harness.app = null;
- harness.finish();
- }
+ public void destroy() {
+ harness().destroy();
}
- private void removeSplashScreen() {
- if (splashImageView != null && frameLayout != null) {
- harness().runOnUiThread(new Runnable() {
- @Override
- public void run() {
- splashImageView.setVisibility(View.INVISIBLE);
- frameLayout.removeView(splashImageView);
- }
- });
- }
+ @Override
+ public void gainFocus() {
+ harness().gainFocus();
+ }
+
+ @Override
+ public void loseFocus() {
+ harness().loseFocus();
+ }
+
+ @Override
+ public void handleError(String errorMsg, Throwable throwable) {
+ harness().handleError(errorMsg, throwable);
}
}
}
From 9b86ba633f68c4d810381f1fe18fec7ad0b65fa2 Mon Sep 17 00:00:00 2001
From: Davis Rollman
Date: Thu, 14 May 2026 23:33:26 -0700
Subject: [PATCH 4/5] Fix AndroidX stubs for merged Javadoc
---
build.gradle | 1 +
1 file changed, 1 insertion(+)
diff --git a/build.gradle b/build.gradle
index 3a1395207c..1f7b5f7f51 100644
--- a/build.gradle
+++ b/build.gradle
@@ -180,6 +180,7 @@ tasks.register('mergedJavadoc', Javadoc) {
options.overview = file("javadoc-overview.html")
source = mergedJavadocSubprojects.collect { project(it).sourceSets.main.allJava }
+ source += fileTree("jme3-android/src/androidx-stubs/java")
classpath = files(mergedJavadocSubprojects.collect { project(it).sourceSets.main.compileClasspath })
}
From 0bdbe93f299cccf13a7f72bb6d9030ea0494d7bb Mon Sep 17 00:00:00 2001
From: Davis Rollman
Date: Fri, 15 May 2026 20:24:37 -0700
Subject: [PATCH 5/5] Use typed AndroidX fragment stubs
---
.../fragment/app/FragmentActivity.java | 4 ++
.../fragment/app/FragmentManager.java | 49 +++++++++++++++++++
.../fragment/app/FragmentTransaction.java | 49 +++++++++++++++++++
.../java/com/jme3/app/AndroidHarness.java | 35 +++++--------
4 files changed, 115 insertions(+), 22 deletions(-)
create mode 100644 jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentManager.java
create mode 100644 jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentTransaction.java
diff --git a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java
index 4a1ad4af46..e93893efe9 100644
--- a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java
+++ b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java
@@ -40,4 +40,8 @@
* application. This class is excluded from jme3-android artifacts.
*/
public class FragmentActivity extends Activity {
+
+ public FragmentManager getSupportFragmentManager() {
+ return null;
+ }
}
diff --git a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentManager.java b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentManager.java
new file mode 100644
index 0000000000..5ae28b71d8
--- /dev/null
+++ b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentManager.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2009-2026 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package androidx.fragment.app;
+
+/**
+ * Compile-time stub for the AndroidX FragmentManager API.
+ *
+ * The real AndroidX Fragment dependency must be supplied by the Android
+ * application. This class is excluded from jme3-android artifacts.
+ */
+public class FragmentManager {
+
+ public Fragment findFragmentByTag(String tag) {
+ return null;
+ }
+
+ public FragmentTransaction beginTransaction() {
+ return null;
+ }
+}
diff --git a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentTransaction.java b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentTransaction.java
new file mode 100644
index 0000000000..338cbe64c1
--- /dev/null
+++ b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentTransaction.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2009-2026 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package androidx.fragment.app;
+
+/**
+ * Compile-time stub for the AndroidX FragmentTransaction API.
+ *
+ * The real AndroidX Fragment dependency must be supplied by the Android
+ * application. This class is excluded from jme3-android artifacts.
+ */
+public class FragmentTransaction {
+
+ public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
+ return this;
+ }
+
+ public int commit() {
+ return 0;
+ }
+}
diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
index 7595fb952e..59d259c707 100644
--- a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
+++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
@@ -44,7 +44,9 @@
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
import com.jme3.audio.AudioRenderer;
import com.jme3.input.JoyInput;
import com.jme3.input.TouchInput;
@@ -58,7 +60,6 @@
import com.jme3.system.android.OGLESContext;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -112,7 +113,6 @@ public class AndroidHarness extends FragmentActivity
protected ImageView splashImageView;
protected FrameLayout frameLayout;
- private HarnessFragment fragment;
private boolean firstDrawFrame = true;
@Override
@@ -120,7 +120,7 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureWindow();
- fragment = attachFragment();
+ HarnessFragment fragment = attachFragment();
fragment.setFinishOnAppStop(finishOnAppStop);
}
@@ -139,26 +139,17 @@ public Application getJmeApplication() {
}
private HarnessFragment attachFragment() {
- try {
- Method getSupportFragmentManager = getClass().getMethod("getSupportFragmentManager");
- Object fragmentManager = getSupportFragmentManager.invoke(this);
- Object existingFragment = fragmentManager.getClass()
- .getMethod("findFragmentByTag", String.class)
- .invoke(fragmentManager, HARNESS_FRAGMENT_TAG);
- if (existingFragment instanceof HarnessFragment) {
- return (HarnessFragment) existingFragment;
- }
-
- HarnessFragment newFragment = new HarnessFragment();
- Object transaction = fragmentManager.getClass().getMethod("beginTransaction").invoke(fragmentManager);
- transaction = transaction.getClass()
- .getMethod("replace", int.class, androidx.fragment.app.Fragment.class, String.class)
- .invoke(transaction, android.R.id.content, newFragment, HARNESS_FRAGMENT_TAG);
- transaction.getClass().getMethod("commit").invoke(transaction);
- return newFragment;
- } catch (ReflectiveOperationException exception) {
- throw new IllegalStateException("Unable to attach AndroidHarnessFragment", exception);
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ Fragment existingFragment = fragmentManager.findFragmentByTag(HARNESS_FRAGMENT_TAG);
+ if (existingFragment instanceof HarnessFragment) {
+ return (HarnessFragment) existingFragment;
}
+
+ HarnessFragment newFragment = new HarnessFragment();
+ fragmentManager.beginTransaction()
+ .replace(android.R.id.content, newFragment, HARNESS_FRAGMENT_TAG)
+ .commit();
+ return newFragment;
}
@Override