Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ jobs:
java-version: '25'
- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@v6.3.0
# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Run Checkstyle
run: |
./gradlew checkstyleMain checkstyleTest --console=plain --stacktrace
Expand Down Expand Up @@ -95,6 +101,12 @@ jobs:
java-version: '25'
- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@v6.3.0
# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Run SpotBugs
run: |
./gradlew -PenableSpotBugs=true spotbugsMain spotbugsTest --console=plain --stacktrace
Expand Down Expand Up @@ -122,6 +134,12 @@ jobs:
java-version: '25'
- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@v6.3.0
# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Run Javadoc doclint
run: |
./gradlew -PenableJavadocError=true javadoc mergedJavadoc --console=plain --stacktrace
Expand Down Expand Up @@ -341,6 +359,12 @@ jobs:

- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@v6.3.0
# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Build Engine
shell: bash
run: |
Expand Down Expand Up @@ -424,6 +448,12 @@ jobs:
distribution: 'temurin'
java-version: '25'

# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Rebuild the maven artifacts and upload them to Sonatype's maven-snapshots repo
env:
ORG_GRADLE_PROJECT_centralUsername: ${{ secrets.CENTRAL_USERNAME }}
Expand Down Expand Up @@ -489,6 +519,12 @@ jobs:
name: release
path: dist/release

# jme3-android compiles against the platform android.jar of the Android SDK
- name: Set up the Android SDK
uses: android-actions/setup-android@v4.0.4
with:
packages: 'platforms;android-34'

- name: Rebuild the maven artifacts and close the Sonatype staging repository
env:
ORG_GRADLE_PROJECT_centralUsername: ${{ secrets.CENTRAL_USERNAME }}
Expand Down
7 changes: 7 additions & 0 deletions jme3-android-examples/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
android:launchMode="singleTask"
android:screenOrientation="landscape">
</activity>
<activity
android:name=".TestGameModeActivity"
android:exported="true"
android:label="Test Game Mode Activity"
android:launchMode="singleTask"
android:screenOrientation="landscape">
</activity>
</application>

<!-- Tell the system that you need ES 3.0. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.jmonkeyengine.jme3androidexamples;

import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import androidx.fragment.app.FragmentActivity;
import com.jme3.app.LegacyApplication;
import com.jme3.system.android.GameMode;
import com.jme3.system.android.OnGameModeChanged;
import com.jme3.view.surfaceview.JmeSurfaceView;

/**
* Example and verification Activity reporting the Android Game Mode selected by the user.
*
* <p>It registers an {@link OnGameModeChanged} listener on the {@link JmeSurfaceView} and
* logs every game mode change. The same listener is available on
* {@code com.jme3.app.AndroidHarnessFragment}.</p>
*
* <p>The platform only reports a game mode on Android 12 and newer, and only for
* applications it treats as games; everywhere else the listener is notified once with
* {@link GameMode#UNSUPPORTED}. See the Android documentation for the
* <a href="https://developer.android.com/games/gamemode/about-API-and-interventions">Game Mode API</a>.</p>
*
* <p>Launch it for example with:
* {@code adb shell am start -n org.jmonkeyengine.jme3androidexamples/.TestGameModeActivity}
* and watch the output with {@code adb logcat -s TestGameModeActivity}. Add
* {@code --es Selected_App_Class <class>} to host a different jME application.</p>
*
* @see GameMode
* @see OnGameModeChanged
* @see JmeSurfaceView#setOnGameModeChanged(OnGameModeChanged)
*/
@SuppressWarnings("deprecation")
public class TestGameModeActivity extends FragmentActivity {

/**
* Key of the intent extra selecting the jME application to host. It mirrors
* {@code MainActivity.SELECTED_APP_CLASS}.
*/
private static final String SELECTED_APP_CLASS = "Selected_App_Class";

private static final String TAG = "TestGameModeActivity";
private static final String DEFAULT_APP_CLASS = "jme3test.android.TestAndroidSensors";

private JmeSurfaceView jmeSurfaceView;

@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
super.onCreate(savedInstanceState);

jmeSurfaceView = new JmeSurfaceView(this);
jmeSurfaceView.setOnGameModeChanged(new OnGameModeChanged() {
@Override
public void onGameModeChanged(GameMode gameMode) {
/*
* The callback is invoked with the current mode as soon as the listener
* is registered and afterwards on every change made in the system game
* settings. Applications can implement their own logic here, for example
* altering the level of detail, loading lower-poly models, changing the
* frame rate or disabling filters.
*/
System.out.println("Game mode changed to: " + gameMode);
Log.i(TAG, "Game mode changed to: " + gameMode);
switch (gameMode) {
case PERFORMANCE:
// Favor visual quality, for example a higher frame rate.
break;
case BATTERY:
// Save power, for example a lower frame rate and no filters.
break;
case STANDARD:
// Use the regular, balanced settings.
break;
case UNSUPPORTED:
default:
// The Game Mode API is not available on this device.
break;
}
}
});

String appClass = DEFAULT_APP_CLASS;
Bundle bundle = getIntent().getExtras();
if (bundle != null && bundle.containsKey(SELECTED_APP_CLASS)) {
appClass = bundle.getString(SELECTED_APP_CLASS);
}

try {
Class<?> clazz = Class.forName(appClass);
LegacyApplication app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
jmeSurfaceView.setLegacyApplication(app);
} catch (Exception e) {
throw new RuntimeException(e);
}

getLifecycle().addObserver(jmeSurfaceView);

RelativeLayout layout = new RelativeLayout(this);
layout.addView(jmeSurfaceView);
setContentView(layout);

jmeSurfaceView.startRenderer(0);
}
}
63 changes: 61 additions & 2 deletions jme3-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,74 @@ sourceSets {
}
}

// The android.* classes come from the platform android.jar of the locally installed Android
// SDK, see https://github.com/jMonkeyEngine/jmonkeyengine/issues/1148. The SDK is looked up
// the same way settings.gradle does it for the Android examples, and the jar is resolved in a
// provider so that the build only fails when this module is really compiled (or its javadoc
// or its classpath is built) instead of on every Gradle invocation.
def androidMinCompileSdk = 34
def findAndroidSdk = {
def sdkDirs = []

if (project.hasProperty('android.sdk.path')) {
sdkDirs << file(project.property('android.sdk.path'))
}

def localProperties = rootProject.file('local.properties')
if (localProperties.isFile()) {
Properties properties = new Properties()
localProperties.withInputStream { properties.load(it) }
if (properties.getProperty('sdk.dir')) {
sdkDirs << file(properties.getProperty('sdk.dir'))
}
}

if (System.env.ANDROID_HOME) {
sdkDirs << file(System.env.ANDROID_HOME)
}
if (System.env.ANDROID_SDK_ROOT) {
sdkDirs << file(System.env.ANDROID_SDK_ROOT)
}
sdkDirs << file("${System.properties['user.home']}/Android/Sdk")
sdkDirs << file("${System.properties['user.home']}/Library/Android/sdk")
sdkDirs << file("${System.properties['user.home']}/AppData/Local/Android/Sdk")

for (sdkDir in sdkDirs.unique { it.absolutePath }) {
def compileSdk = new File(sdkDir, 'platforms').listFiles()?.collect { platform ->
def matcher = platform.name =~ /^android-(\d+)$/
matcher.matches() && new File(platform, 'android.jar').isFile() ? matcher[0][1] as int : null
}?.findAll { it != null }?.max()

if (sdkDir.isDirectory() && compileSdk != null && compileSdk >= androidMinCompileSdk) {
return [dir: sdkDir, compileSdk: compileSdk]
}
}

return null
}

def androidJar = providers.provider {
def androidSdk = findAndroidSdk()
if (androidSdk == null) {
throw new GradleException(
"No Android SDK with API level ${androidMinCompileSdk} or newer was found. "
+ "jme3-android compiles against the platform android.jar shipped with the Android SDK: "
+ "install it with 'sdkmanager \"platforms;android-${androidMinCompileSdk}\"' and make the "
+ "SDK discoverable through the 'android.sdk.path' project property, 'sdk.dir' in "
+ "local.properties, or the ANDROID_HOME / ANDROID_SDK_ROOT environment variables.")
}
return new File(androidSdk.dir, "platforms/android-${androidSdk.compileSdk}/android.jar")
}

dependencies {
//added annotations used by JmeSurfaceView.
compileOnly libs.androidx.annotation
compileOnly libs.androidx.lifecycle.common
compileOnly sourceSets.androidxStubs.output
androidxStubsCompileOnly files(rootProject.file('lib/android.jar'))
androidxStubsCompileOnly files(androidJar)
api project(':jme3-core')
implementation libs.jme3.android.natives
compileOnly files(rootProject.file('lib/android.jar'))
compileOnly files(androidJar)
}

compileJava {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@
import com.jme3.input.android.AndroidJoyInput;
import com.jme3.system.AppSettings;
import com.jme3.system.SystemListener;
import com.jme3.system.android.AndroidGameMode;
import com.jme3.system.android.GameMode;
import com.jme3.system.android.JmeAndroidSystem;
import com.jme3.system.android.OGLESContext;
import com.jme3.system.android.OnGameModeChanged;
import com.jme3.util.AndroidLogHandler;
import com.jme3.util.AndroidNativeBufferAllocator;
import com.jme3.util.BufferAllocatorFactory;
Expand All @@ -71,10 +74,14 @@ public abstract class AndroidHarnessFragment extends Fragment implements SystemL
protected GLSurfaceView view;
protected LegacyApplication app;
protected boolean finishOnAppStop = true;
private Context attachedContext;
private AndroidGameMode androidGameMode;
private OnGameModeChanged onGameModeChangedListener;

@Override
public void onAttach(Context context) {
super.onAttach(context);
attachedContext = context;
}

public Application getJmeApplication() {
Expand All @@ -85,6 +92,38 @@ public void setFinishOnAppStop(boolean finishOnAppStop) {
this.finishOnAppStop = finishOnAppStop;
}

/**
* Registers a listener notified when the Android game mode changes.
*
* <p>The current game mode is reported to the listener as soon as it is registered,
* including once with {@link GameMode#UNSUPPORTED} on devices where the Game Mode API
* is unavailable (Android 11 and older) or for applications the platform does not
* treat as games. Pass null to unregister a previously registered listener. When this
* method is called before the fragment is attached to a context, the listener is
* registered as soon as the fragment is created.</p>
*
* <p>Applications typically use this listener to alter the level of detail, load
* lower-poly models, change the frame rate or disable filters when the platform asks
* for performance or for battery saving.</p>
*
* @param onGameModeChanged the listener, or null to unregister
* @see GameMode
* @see OnGameModeChanged
*/
public void setOnGameModeChanged(OnGameModeChanged onGameModeChanged) {
this.onGameModeChangedListener = onGameModeChanged;
if (attachedContext != null) {
getAndroidGameMode().setListener(onGameModeChanged);
}
}

private AndroidGameMode getAndroidGameMode() {
if (androidGameMode == null) {
androidGameMode = new AndroidGameMode(attachedContext);
}
return androidGameMode;
}

@Override
public void onCreate(Bundle savedInstanceState) {
initializeLogHandler();
Expand All @@ -106,6 +145,10 @@ public void onCreate(Bundle savedInstanceState) {
} catch (Exception exception) {
handleError("jME application initialization failed", exception);
}

if (onGameModeChangedListener != null) {
getAndroidGameMode().setListener(onGameModeChangedListener);
}
}

/**
Expand Down Expand Up @@ -166,6 +209,11 @@ public void onDestroyView() {
@Override
public void onDestroy() {
logger.fine("onDestroy");
if (androidGameMode != null) {
androidGameMode.setListener(null);
androidGameMode = null;
}
attachedContext = null;
if (app != null) {
app.stop(false);
}
Expand Down
Loading
Loading