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