Maven And Android

From JogampWiki
Revision as of 16:04, 7 January 2013 by Mraynsford (talk | contribs) (Additional images and command line install)
Jump to navigation Jump to search

Managing your Android JOGL projects with Maven is, if not effortless, at least not difficult. The following article attempts to describe one method of producing a project that will compile and run unmodified on both Android and ordinary "desktop" systems.

The article presents a demo that illustrates the organization of a trivial OpenGL application, and covers the rationale and methods behind its construction.

Maven Modules

The intention in this article is to develop an extremely simple OpenGL program that simply clears the screen to a garish colour at sixty frames per second.

Typically, software projects have handled platform-specific code by using some form of conditional compilation. That is, the build system usually contains some sort of "if the current platform is P, then compile A, else compile B" statements. The recommended way to handle this with Maven is to break a project up into separate modules so that any platform specific code is isolated and can be enabled or disabled on a per-module basis.

Assuming then that the intention is to get the program to run unmodified on Android and "anything else that can create an OpenGL window", the first thing that needs to be done is the break the program up into separate parts. Specifically:

  • The "Android" frontend (the Activity, and associated code and classes)
  • The "desktop" frontend (the usual window and context creation code, called from the main procedure in typical programs)
  • The platform-independent OpenGL core

Project

Firstly, a simple POM file is written that declares three submodules and very little else. As can probably be seen from the source browser, each of the modules has a corresponding directory each containing POM files that will be described further on in the article.

This demo project is named jp4da. Prizes won't be awarded for working out what the acronym means!

Core

The platform-independent part of the program is probably the simplest, and is contained in the jp4da-core module.

A simple Example class that implements GLEventListener is all that's required.

The POM file for the jp4da-core module is very simple, but does have one unusual distinction: The declared dependencies for the project are, at first glance, somewhat lacking. Typically (according to the published instructions on setting up a JOGL Maven project), projects will explicitly depend on the jogl-all-main and gluegen-rt-main packages in order to have Maven automatically retrieve and use the native libraries associated with each project.

The POM file for jp4da-core clearly doesn't do this.

  <dependencies>
    <dependency>
      <groupId>org.jogamp.jogl</groupId>
      <artifactId>jogl-all</artifactId>
      <version>2.0-rc11post03</version>
    </dependency>
  </dependencies>

The reason for this is simple: Depending on a specific set of native libraries would be implicitly stating that module is intended to run on a particular platform, as opposed to being platform-independent. Instead, the module essentially just depends on the one JOGL package that will allow the Java compiler to type-check and compile the code, without giving any consideration to how the final application will be constructed. It will also become clear, when describing the Android module, why depending on the native library packages here would be actively harmful!

Desktop

The jp4da-desktop module implements an extremely simple main program that opens a NEWT window and runs the event listener implemented in the jp4da-core module.

The POM file is similarly unsurprising, and simply declares some dependencies on the native packages that were specifically ignored earlier.

Android

The jp4da-android module implements the Android front-end for the program.

The main Activity is very similar to the desktop code: It simply opens a window and instantiates the core's event listener class. None of this code should be particularly surprising to an Android or JOGL programmer.

The POM file for the project is, however, not quite so simple. The project file attempts to solve three separate problems:

  • Process all class files with the various Android tools required to produce Dalvik bytecode
  • Organize the correct dependencies on JOGL itself, excluding "bad" dependencies that Maven implicitly adds.
  • Produce a single APK package that follows the correct layout required by JOGL itself and includes all application assets

Processing class files

Thankfully, as of version 3.5.0, the Android Maven plugin handles all the necessary processing of class files without requiring any configuration on the part of the developer (apart from, perhaps, setting the ANDROID_HOME environment variable to point to the SDK tools).

With the plugin added to the build section of the POM file, no further configuration is required to process class files.

Dependencies

For running code on Android, JOGL provides separate jogl-all-android and gluegen-rt-android jar files. However, this is where the first problem arises due to Maven's transitive dependency handling: The jp4da-android project depends on jp4da-core which depends on jogl-all. The jp4da-android module will therefore also implicitly depend on jogl-all, which will now conflict with jogl-all-android!

There are two accepted methods to handle this problem:

  • Make jogl-all an optional dependency of the jp4da-core project. It is then the responsibility of jp4da-android to provide a dependency that satisfies all constraints. Or:
  • Have the jp4da-android module explicitly ignore the jogl-all dependency of jp4da-core.

Neither method has any particular advantages or disadvantages in this case, so in the interest of keeping things explicit, the second method was chosen. The jp4da-android module explicitly ignores the jogl-all dependency and provides its own instead:

  <dependencies>
    <dependency>
      <groupId>com.io7m.examples.jp4da</groupId>
      <artifactId>jp4da-core</artifactId>
      <version>1.0.0</version>

      <!--
        The jp4da-core project depends on jogl-all in order to type-check/compile,
        and therefore will be added as a transitive dependency of this project. However,
        this is an Android package and wants to use the Android JOGL package. Obviously,
        it's not possible to use both, so this exclusion tells Maven that the dependency
        introduced by jp4da-core on JOGL should be ignored.
        -->

      <exclusions>
        <exclusion>
          <groupId>org.jogamp.jogl</groupId>
          <artifactId>jogl-all</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

    <dependency>
      <groupId>com.google.android</groupId>
      <artifactId>android</artifactId>
      <version>1.6_r2</version>
      <scope>provided</scope>
    </dependency>

    <!-- Depend on the gluegen runtime Android package -->
    <dependency>
      <groupId>org.jogamp.gluegen</groupId>
      <artifactId>gluegen-rt-android</artifactId>
      <version>2.0-rc11post03</version>
    </dependency>
    <!-- Depend on the correct natives for Android -->
    <dependency>
      <groupId>org.jogamp.gluegen</groupId>
      <artifactId>gluegen-rt</artifactId>
      <version>2.0-rc11post03</version>
      <classifier>natives-android-armv6</classifier>
    </dependency>
    <!-- Depend on the jogl Android package -->
    <dependency>
      <groupId>org.jogamp.jogl</groupId>
      <artifactId>jogl-all-android</artifactId>
      <version>2.0-rc11post03</version>
    </dependency>
    <!-- Depend on the correct natives for Android -->
    <dependency>
      <groupId>org.jogamp.jogl</groupId>
      <artifactId>jogl-all</artifactId>
      <version>2.0-rc11post03</version>
      <classifier>natives-android-armv6</classifier>
      <scope>runtime</scope>
    </dependency>
  </dependencies>

Producing an APK

Finally, it's necessary to actually produce an APK that can be installed onto devices.

The project as described so far will not work when installed, both because JOGL requires that certain files appear in specific places inside the APK and because native libraries have not been placed in the correct directory to be packaged. Additionally, the Android Maven plugin will, by default, include everything that isn't a class file from every dependency jar on the classpath into the final APK.

Excluding redundant files

So, firstly, the Android Maven plugin needs to be told not to include anything from the JOGL and GlueGen jar files. This is achieved by giving a regular expression against which the names of each jar file on the classpath will be tested. If a jar file is matched then the contents of the jar file are not included in the resulting APK. Essentially, specific jar files are blacklisted.

      <!--
        The default behaviour for the Android Maven plugin is to
        attempt to include everything that isn't a class file from every
        dependency jar. Because we already manually included the assets
        and native libraries using the "dependency" plugin above, it's
        necessary to tell the Android Maven plugin to exclude the jar
        files matching the given patterns from the inclusion process.

        This saves roughly 4mb of redundant files from being included
        in the final APK, at the time of writing.
        -->

      <plugin>
        <groupId>com.jayway.maven.plugins.android.generation2</groupId>
        <artifactId>android-maven-plugin</artifactId>
        <configuration>
          <sdk>
            <platform>10</platform>
          </sdk>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>apk</goal>
            </goals>
            <configuration>
              <excludeJarResources>
                <excludeJarResource>jogl-.*\.jar$</excludeJarResource>
                <excludeJarResource>gluegen-rt-.*\.jar$</excludeJarResource>
              </excludeJarResources>
            </configuration>
          </execution>
        </executions>
      </plugin>

Copying assets and native libraries

Then, the standard Maven dependency plugin is used to unpack assets into the assets directory (so that the Android Maven plugin can find and process them), and also to unpack and place native libraries into the correct libs directory (again, so that the Android Maven plugin can find and process them).

The process is simple, and nothing that should be surprising to any reasonably experienced Maven user:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.6</version>
        <executions>

          <!-- Unpack the JOGL natives -->
          <execution>
            <id>unpack-jogl-natives</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>unpack</goal>
            </goals>
            <configuration>
              <artifactItems>
                <artifactItem>
                  <groupId>org.jogamp.jogl</groupId>
                  <artifactId>jogl-all</artifactId>
                  <version>2.0-rc11post03</version>
                  <classifier>natives-android-armv6</classifier>
                  <overWrite>true</overWrite>
                  <outputDirectory>${project.basedir}/libs/armeabi</outputDirectory>
                  <includes>lib*.so</includes>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>

          <!-- Unpack the JOGL assets -->
          <!-- In other words, copy anything from the jar file that isn't a class file -->
          <execution>
            <id>unpack-jogl-assets</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>unpack</goal>
            </goals>
            <configuration>
              <artifactItems>
                <artifactItem>
                  <groupId>org.jogamp.jogl</groupId>
                  <artifactId>jogl-all-android</artifactId>
                  <version>2.0-rc11post03</version>
                  <overWrite>true</overWrite>
                  <outputDirectory>${project.basedir}/assets</outputDirectory>
                  <excludes>**/*.class</excludes>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>

          <!-- Unpack the GlueGen natives -->
          <execution>
            <id>unpack-gluegen-natives</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>unpack</goal>
            </goals>
            <configuration>
              <artifactItems>
                <artifactItem>
                  <groupId>org.jogamp.gluegen</groupId>
                  <artifactId>gluegen-rt</artifactId>
                  <version>2.0-rc11post03</version>
                  <classifier>natives-android-armv6</classifier>
                  <overWrite>true</overWrite>
                  <outputDirectory>${project.basedir}/libs/armeabi</outputDirectory>
                  <includes>lib*.so</includes>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>
        </executions>
      </plugin>

Cleaning up

Finally, because the project has performed some rather non-standard (from the perspective of typical Maven usage) operations, it's good practice to use the Maven Clean plugin to clean up the manually unpacked files so that each build always starts from a completely clean slate.

      <!--
        Next, because there have been files unpacked to non-standard
        locations, it's necessary to tell the "clean" plugin to delete
        the unpacked files (for the sake of keeping things tidy).
        -->

      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>${project.basedir}/libs/armeabi</directory>
              <includes>
                <include>libgluegen-rt.so</include>
                <include>libnewt.so</include>
                <include>libjogl_mobile.so</include>
              </includes>
              <followSymlinks>false</followSymlinks>
            </fileset>

            <!--
              Note that if you don't use the "assets" directory
              for anything else in your project, you can probably
              just delete the whole thing, rather than picking
              things carefully in the manner shown here.
              -->

            <fileset>
              <directory>${project.basedir}/assets</directory>
              <includes>
                <include>META-INF/**</include>
                <include>com/**</include>
                <include>jogl/**</include>
                <include>jogamp/**</include>
                <include>javax/**</include>
              </includes>
              <followSymlinks>false</followSymlinks>
            </fileset>
          </filesets>
        </configuration>
      </plugin>

Building

Running a build from the main project directory should proceed without issue:

$ mvn -C clean verify
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO] 
[INFO] jp4da-core
[INFO] jp4da-desktop
[INFO] jp4da-android
[INFO] jp4da
...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] jp4da-core ........................................ SUCCESS [1.905s]
[INFO] jp4da-desktop ..................................... SUCCESS [0.658s]
[INFO] jp4da-android ..................................... SUCCESS [21.129s]
[INFO] jp4da ............................................. SUCCESS [0.001s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 24.688s
[INFO] Finished at: Fri Jan 04 23:06:47 GMT 2013
[INFO] Final Memory: 22M/236M
[INFO] ------------------------------------------------------------------------
$

The jp4da-android/target directory now contains a jp4da-android.apk file that can be installed onto devices or the Android emulator.

$ adb install jp4da-android/target/jp4da-android.apk 
1026 KB/s (2334537 bytes in 2.221s)
	pkg: /data/local/tmp/jp4da-android.apk
Success

Highly complex rendering

The installed application

The running application