Anyone tried Gluon Client Plugin for Android/iOS?

Hi guys

Has anyone tried JME on Android/iOS with Gluon Client Gradle plugin and GraalVM?

https://docs.gluonhq.com/client/

1 Like

Yes, it was a disaster. I’m at work, but I will do a write up on it later in the next couple days.
The Gradle plugin is very broken, it is much, much simpler just to do by hand what the plugin tries to do.

3 Likes

Thanks for reply

Do you mean manually with GraalVM? or with SubstrateVM?

Could you generate an apk? Does it run fine on the phone?

Thanks, that would be cool :grinning:

@tlf30 any news?

Still interested to hear about your experience. :slightly_smiling_face:

@Ali_RS Yes! I just flew home last night. I’m trying to recuperate some energy now, three weeks in the artic at work was brutal. This felt like the longest three weeks ever. The only good thing is that spring is almost ready to come out up there, we saw some days above 0 degF finally! And the caribou are starting to come out. It was a brutally cold winter, we broke our 20 year lows, so it will be nice next hitch up to see some warmer weather and the tundra start to come to life.

I will try to get something written up today or tomorrow.

5 Likes

Ah, welcome home! Please stay safe and warm and take a good rest. :slightly_smiling_face:

I appreciate your help

1 Like

So I dove back into this today, and built a working example. The issue starts at where Gluon left off on JavaFX Ports. They still only support JavaFX 8, meaning you cannot use @jayfella 's awesome
jme-jfx-11 lib.

Unless of course someone knows of a port for jfx11? I have been unable to find one.

To get started in JavaFx 8 with Jme, it is not too bad (once you get around all of the android build issues)

First, spin up android studio, and create a new blank project.
Then download the dalvik-sdk from here: https://gluonhq.com/products/mobile/javafxports/get/
Extract it into the top level of your app in a dalvik-sdk folder.

Now, you are going to need to edit the gradle build files.
The top level one:

// Top-level build file where you can add configuration options common to all sub-projects/modules.


buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        mavenLocal()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

The one in the app/ folder

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId 'com.example.myjmejfxapp'
        minSdkVersion 28
        targetSdkVersion 28
        versionCode 1
        versionName "0.0.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true
    }
    lintOptions {
        // Fix nifty gui referencing "java.awt" package.
        disable 'InvalidPackage'
        abortOnError false
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main {
            jniLibs.srcDir file("../dalvik-sdk/rt/lib")
            assets.srcDirs = ['assets']
        }
    }
    dexOptions {
        preDexLibraries = false
        additionalParameters = ['--core-library']
    }
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'android.arch.lifecycle:extensions:1.1.1'
    implementation 'com.android.support:support-v4:28.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    //JME
    implementation "org.jmonkeyengine:jme3-android:3.3.0-stable"
    implementation "org.jmonkeyengine:jme3-android-native:3.3.0-stable"

    //JFX
    compile fileTree(include: ['*.jar'], dir: '../dalvik-sdk/rt/lib/ext')
    implementation 'com.android.support:multidex:1.0.1'
}

Now, you will want to create a fragment, and two activities. I will just put them here with the layouts:

package com.example.myjmejfxapp;

import android.os.Bundle;
import com.jme3.app.AndroidHarnessFragment;
import java.util.logging.Level;
import java.util.logging.LogManager;


public class JmeJfxFragment extends AndroidHarnessFragment {
    public JmeJfxFragment() {
        // Set the desired EGL configuration
        eglBitsPerPixel = 24;
        eglAlphaBits = 0;
        eglDepthBits = 16;
        eglSamples = 0;
        eglStencilBits = 0;

        // Set the maximum framerate
        // (default = -1 for unlimited)
        frameRate = -1;

        // Set the maximum resolution dimension
        // (the smaller side, height or width, is set automatically
        // to maintain the original device screen aspect ratio)
        // (default = -1 to match device screen resolution)
        maxResolutionDimension = -1;

        /*
        Skip these settings and use the settings stored in the Bundle retrieved during onCreate.
        // Set main project class (fully qualified path)
        appClass = "";
        // Set input configuration settings
        joystickEventsEnabled = false;
        keyEventsEnabled = true;
        mouseEventsEnabled = true;
        */

        // Set application exit settings
        finishOnAppStop = true;
        handleExitHook = true;
        exitDialogTitle = "Do you want to exit?";
        exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it.";

        // Set splash screen resource id, if used
        // (default = 0, no splash screen)
        // For example, if the image file name is "splash"...
        //     splashPicID = R.drawable.splash;
        splashPicID = 0;
//        splashPicID = R.drawable.android_splash;

        // Set the default logging level (default=Level.INFO, Level.ALL=All Debug Info)
        LogManager.getLogManager().getLogger("").setLevel(Level.INFO);

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Bundle bundle=getArguments();

        appClass = bundle.getString(JmeActivity.SELECTED_APP_CLASS);
        joystickEventsEnabled = bundle.getBoolean(JmeActivity.ENABLE_JOYSTICK_EVENTS);
        keyEventsEnabled = bundle.getBoolean(JmeActivity.ENABLE_KEY_EVENTS);
        mouseEventsEnabled = bundle.getBoolean(JmeActivity.ENABLE_MOUSE_EVENTS);
        boolean verboseLogging = true;
        if (verboseLogging) {
            LogManager.getLogManager().getLogger("").setLevel(Level.ALL);
        } else {
            LogManager.getLogManager().getLogger("").setLevel(Level.INFO);
        }

        super.onCreate(savedInstanceState);
    }
}
package com.example.myjmejfxapp;

import androidx.appcompat.app.AppCompatActivity;

import android.app.FragmentTransaction;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import com.jme3.system.JmeSystem;

public class JmeActivity extends AppCompatActivity {
    JmeJfxFragment fragment;

    /**
     * Static String to pass the key for the selected test app to the
     * TestsHarness class to start the application. Also used to store the
     * current selection to the savedInstanceState Bundle.
     */
    public static final String SELECTED_APP_CLASS = "Selected_App_Class";

    /**
     * Static String to pass the key for the selected list position to the
     * savedInstanceState Bundle so the list position can be restored after
     * exiting the test application.
     */
    public static final String SELECTED_LIST_POSITION = "Selected_List_Position";

    /**
     * Static String to pass the key for the setting for enabling mouse events to the
     * savedInstanceState Bundle.
     */
    public static final String ENABLE_MOUSE_EVENTS = "Enable_Mouse_Events";

    /**
     * Static String to pass the key for the setting for enabling joystick events to the
     * savedInstanceState Bundle.
     */
    public static final String ENABLE_JOYSTICK_EVENTS = "Enable_Joystick_Events";

    /**
     * Static String to pass the key for the setting for enabling key events to the
     * savedInstanceState Bundle.
     */
    public static final String ENABLE_KEY_EVENTS = "Enable_Key_Events";

    /**
     * Static String to pass the key for the setting for verbose logging to the
     * savedInstanceState Bundle.
     */
    public static final String VERBOSE_LOGGING = "Verbose_Logging";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jme);

        fragment = new JmeJfxFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();

        Bundle bundle = savedInstanceState;
        if (bundle == null) {
            bundle = getIntent().getExtras();
        }

        String appClass = bundle.getString(SELECTED_APP_CLASS);
        args.putString(SELECTED_APP_CLASS, appClass);
//        Log.d(TestActivity.class.getSimpleName(), "AppClass="+appClass);

        boolean mouseEnabled = bundle.getBoolean(ENABLE_MOUSE_EVENTS, true);
        args.putBoolean(ENABLE_MOUSE_EVENTS, mouseEnabled);
//        Log.d(TestActivity.class.getSimpleName(), "MouseEnabled="+mouseEnabled);

        boolean joystickEnabled = bundle.getBoolean(ENABLE_JOYSTICK_EVENTS, true);
        args.putBoolean(ENABLE_JOYSTICK_EVENTS, joystickEnabled);
//        Log.d(TestActivity.class.getSimpleName(), "JoystickEnabled="+joystickEnabled);

        boolean keyEnabled = bundle.getBoolean(ENABLE_KEY_EVENTS, true);
        args.putBoolean(ENABLE_KEY_EVENTS, keyEnabled);
//        Log.d(TestActivity.class.getSimpleName(), "KeyEnabled="+keyEnabled);

        boolean verboseLogging = bundle.getBoolean(VERBOSE_LOGGING, true);
        args.putBoolean(VERBOSE_LOGGING, verboseLogging);
//        Log.d(TestActivity.class.getSimpleName(), "VerboseLogging="+verboseLogging);

        fragment.setArguments(args);


        FragmentTransaction transaction = getFragmentManager().beginTransaction();

        // Replace whatever is in the fragment_container view with this fragment,
        // and add the transaction to the back stack so the user can navigate back
        transaction.add(R.id.fragmentContainer, fragment);
        transaction.addToBackStack(null);

        // Commit the transaction
        transaction.commit();

    }


    private void toggleKeyboard(final boolean show) {
        fragment.getView().getHandler().post(new Runnable() {

            @Override
            public void run() {
                JmeSystem.showSoftKeyboard(show);
            }
        });
    }
}
package com.example.myjmejfxapp;

import android.annotation.SuppressLint;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;

public class FullscreenActivity extends AppCompatActivity {


    /**
     * Some older devices needs a small delay between UI widget updates
     * and a change of the status and navigation bar.
     */
    private static final int UI_ANIMATION_DELAY = 300;
    private final Handler mHideHandler = new Handler();
    private View mContentView;
    private final Runnable mHidePart2Runnable = new Runnable() {
        @SuppressLint("InlinedApi")
        @Override
        public void run() {
            // Delayed removal of status and navigation bar

            // Note that some of these constants are new as of API 16 (Jelly Bean)
            // and API 19 (KitKat). It is safe to use them, as they are inlined
            // at compile-time and do nothing on earlier devices.
            mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
        }
    };
    private View mControlsView;
    private final Runnable mShowPart2Runnable = new Runnable() {
        @Override
        public void run() {
            // Delayed display of UI elements
            ActionBar actionBar = getSupportActionBar();
            if (actionBar != null) {
                actionBar.show();
            }
            mControlsView.setVisibility(View.VISIBLE);
        }
    };
    private boolean mVisible;

    /**
     * Touch listener to use for in-layout UI controls to delay hiding the
     * system UI. This is to prevent the jarring behavior of controls going away
     * while interacting with activity UI.
     */
    private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            Intent intent = new Intent(FullscreenActivity.this, JmeActivity.class);

            Bundle args = new Bundle();

            args.putString(JmeActivity.SELECTED_APP_CLASS, "com.example.myjmejfxapp.JmeApplication");
            args.putBoolean(JmeActivity.ENABLE_MOUSE_EVENTS, true);
            args.putBoolean(JmeActivity.ENABLE_JOYSTICK_EVENTS, true);
            args.putBoolean(JmeActivity.ENABLE_KEY_EVENTS, true);
            args.putBoolean(JmeActivity.VERBOSE_LOGGING, true);

            intent.putExtras(args);

            startActivity(intent);
            return false;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_fullscreen);

        mVisible = true;
        mControlsView = findViewById(R.id.fullscreen_content_controls);
        mContentView = findViewById(R.id.fullscreen_content);


        findViewById(R.id.launch_button).setOnTouchListener(mDelayHideTouchListener);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
    }

    @SuppressLint("InlinedApi")
    private void show() {
        // Show the system bar
        mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
        mVisible = true;

        // Schedule a runnable to display UI elements after a delay
        mHideHandler.removeCallbacks(mHidePart2Runnable);
        mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
    }

}

The FullscreenActivity you will want to create as a Launcher activity.

activity_fullscreen.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#0099cc"
    tools:context="com.example.myjmejfxapp.FullscreenActivity">

    <!-- The primary full-screen view. This can be replaced with whatever view
         is needed to present your content, e.g. VideoView, SurfaceView,
         TextureView, etc. -->
    <TextView
        android:id="@+id/fullscreen_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:keepScreenOn="true"
        android:text="@string/dummy_content"
        android:textColor="#33b5e5"
        android:textSize="50sp"
        android:textStyle="bold" />

    <!-- This FrameLayout insets its children based on system windows using
         android:fitsSystemWindows. -->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <LinearLayout
            android:id="@+id/fullscreen_content_controls"
            style="?metaButtonBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center_horizontal"
            android:background="@color/black_overlay"
            android:orientation="horizontal"
            tools:ignore="UselessParent">

            <Button
                android:id="@+id/launch_button"
                style="?metaButtonBarButtonStyle"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/dummy_button" />

        </LinearLayout>
    </FrameLayout>

</FrameLayout>

activity_jme.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:keepScreenOn="true"
    tools:context="com.example.myjmejfxapp.JmeActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

Now the fun (really hard) part.
Create a folder called libs/ inside your app/ folder.
Then place your app’s jar and its dependencies in it. You DO NOT want to include the jme-core/desktop or any javafx dependencies.

Now edit line 67 of the FullscreenActivity.java and put your SimpleApplication full class name there.

Remember, your app CANNOT use awt/swing. It also CANNOT have javafx shadowed into it (you will get lots of errors)

Remember, you can only use JavaFX 8 in your application.

2 Likes

From what I have been reading it looks like there is a different approach that gluon is using for javafx 11 with graalVM. I am looking into it, and will post what I find on that.

EDIT: I do not think it is possible to use the Gluon Client/Substrate which uses GraalVM to work in JME w/ android jet. First, the gradle plugin is not complete and currently does not support windows, also lacks any examples on how to use it. Second, the maven plugin only can build android applications from linux.
It looks to me that the plugin only supports JavaFX Application(s). It generates a rendering wrapper depending on the platform, then launches the application. I will continue to investigate how it builds the android wrapper.

2 Likes

OK, I have heavily investigated GraalVM and how to use it.
I have discovered the problem, and it is the same on that this guy found:

After taking apart Gluon’s Client I found the c source files that they compile during the build to ‘glue’ the application to Graal. It is going to take someone with more knowledge than I have to get this all working together.

1 Like

@tlf30 thanks so much for your investigation.

It would be bad news if that’s going to work only with JavaFX. I thought it could be used without JavaFX, as I do not use JavaFX in my app.

Regarding the Gradle plugin, have you seen this:

and yes as you said it seems android build can be done only from Linux currently.

Yes, that was the gluon client I mentioned. It is just a wrapper around some calls from substrate.
They actually have an example of a non-jfx application using just substrate, but I was unsuccessful in figuring out how to make it work with android because substrate still builds the activity wrappers around it.

EDIT: The forum makes it look like a link to the repo, but it is a link to the example

Yes, it was a link to an example on how you can use Gradle plugin. I linked the PR because it had not merged yet, but it seems they merged it just a few minutes a go.

Note, in that example it refers to plugin version 0.1.20

plugins {
    id 'com.gluonhq.client-gradle-plugin' version '0.1.20'
}

which is not released yet, but seems they will release it in a few days.

based on the doc, for building for android we just need to set

```
gluonClient {
     target = "android"
 }
```

and run

```
./gradlew nativePackage
```

to get APK.

It seems straight forward.

1 Like

Oh, I was talking about the link in my post. It is an example on bypassing the plugin and using substrate.

Yeah, the issue I ran into is that it builds its own Activity wrappers around the application. I could not figure out how to use my own activities. Perhaps you can figure it out.

1 Like

Oops! Sorry :man_facepalming:

And yes, will try it out when the new plugin version released.

1 Like

Awesome! If you figure it out, let me know and I will see if I can get it working too.

It will be great once we can get javafx in android. I am utterly amazed at the lack (and incorrectness) of Gluon’s documentation on all of their tools. I have come to conclude that they want you to pay for support to learn how to use them.

1 Like

It was just merged 1 hour ago!

EDIT: Oh, but I do not know if the updated plugin version is out, the PR you linked is merged now. I am searching…

1 Like

I just got linux subsystem for windows 2 installed on my laptop, I will see if I can do the linux builds from it.

Update: I’m noticing bugs already. It has issues downloading the Android NDK automatically and the build crashes with no errors. I had to manually download it.

1 Like

May be related:

btw, they just released new plugin version https://github.com/gluonhq/client-gradle-plugin/releases/tag/0.1.21

1 Like

Yeah, using --info is annoying.
So I am fighting a new issue now that the NDK is working:

> Task :nativeCompile FAILED
Caching disabled for task ':nativeCompile' because:
  Build cache is disabled
Task ':nativeCompile' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
[Thu Apr 23 09:17:20 AKDT 2020][INFO] ==================== COMPILE TASK ====================
:nativeCompile (Thread[Execution worker for ':',5,main]) completed. Took 0.014 secs.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':nativeCompile'.
> Failed to compile

I have tried different gradle versions (5.6, and 6.3). I cannot find anything wrong with the build script.

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java project to get you started.
 * For more details take a look at the Java Quickstart chapter in the Gradle
 * User Manual available at https://docs.gradle.org/5.4/userguide/tutorial_java_projects.html
 */

plugins {
    // Apply the java plugin to add support for Java
    id 'java'

    // Apply the application plugin to add support for building an application
    id 'application'

    id 'com.gluonhq.client-gradle-plugin' version '0.1.21'
}

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

// Define the main class for the application
mainClassName = 'io.tlf.graal.test.App'

gluonClient {
     target = "android"
 }

EDIT: It works when not using the native* stuff:

trevor@Trevorwork:~/graalVmTest$ ./gradlew clean build run

> Task :run
Hello world.

BUILD SUCCESSFUL in 2s
9 actionable tasks: 9 executed

EDIT 2: It fails even when I comment out the target = "android"