Android from scratch, part 2: use android-maven-plugin

Hi people,

I wasn’t sure to write again on Android because the first time I tried the platform I identified 3 knowledge levels:
– academic: knowing what Android is without ever building anything on that platform;
– basic: reading a lot about the topic and sorting information to finally extract the minimum/essential (there are a lot of incomplete/inaccurate information that make them unusable ‘as is’) required to build and deploy an app on a device (cf previous post);
– professional: involves build skills (stable, repeatable, customized per env), developement skills (fat client knowledge is a plus), release/distribution skills (branding, publishing on Google Play, etc)

That last step is mean for professionnals but I’ll try to present the building part. With the right tools we can reach a pretty nice result.

To illustrate our point we’re going to set clear goals:
– we’d like to generate project structure with a template
– we’d like to include third party libraries
– we’d like to filter resources
– we’d like to package
– we’d like to sign, obfuscate, zipalign in a declarative way
– we’d like to run tests as part of the build
– we’d like to release

Ok, you got it, we’re going to use maven. The fact that nothing is “maven standard” in the Android project layer is the difficult part. We’ll have to customize maven defaults.

1 – Create project layer

Some nice guys contributed and made available 3 maven archetypes. The command below lists 3 archetypes

mvn archetype:generate | grep android

The first archetype (android-quick-start) creates a single-module project with the minimum required config to build and deploy a project to a running device
The third one (android-with-test) creates a multi-module project with one module application and its testing module
The second one (android-release) creates a multi-module project that includes the above modules and adds release configuration activated by profiles.
For our need, we choose the second archetype as we want to sign our application with a custom key.

2 – Deploy third party libraries to maven repo

In my sample, I use Google Maps. The jars needed for google maps developement are not in my maven repository, They are provided with the SDK. Manfred Moser (twitter, github) has developped a maven plugin that deploys SDK apis to your local maven repository. You just need to clone the project (from git repository), set Android home and run maven

$ export ANDROID_HOME=/usr/local/android-sdk
$ cd $WORKSPACE
$ git clone https://github.com/mosabua/maven-android-sdk-deployer
$ mvn clean install

If the plugin fails just re-run android sdk manager

$ android&

and re-install the desired SDK.
It fails because from time to time, the Android team updates paths and and renames artifacts, so running the plugin on an old install may lead to a failure. Reinstalling the SDK fixes the deployment.

You can now add google maps to your dependencies

		<dependencies>
			<dependency>
				<groupId>com.google.android</groupId>
				<artifactId>android</artifactId>
				<version>${android.version}</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>com.google.android.maps</groupId>
				<artifactId>maps</artifactId>
				<version>${maps.version}</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>com.google.android</groupId>
				<artifactId>android-test</artifactId>
				<version>${android.version}</version>
				<scope>provided</scope>
			</dependency>
		</dependencies>

3 – The android-maven-plugin

In the project you just generated there are multiple references to android-maven-plugin

<plugin>
    <groupId>com.jayway.maven.plugins.android.generation2</groupId>
    <artifactId>android-maven-plugin</artifactId>
</plugin>

This plugin is the key. It is a wrapper around the provided Android SDK ant tasks.
The plugin documentation presents all available goals and, more important, the phases they are bound to.
If you’re not familiar with maven goals/phases/profiles I would suggest you to upgrade your knowledge because you can not ignore those concepts when creating custum builds.
This is the default maven execution schedule when running “mvn clean install” on a classic project:

maven phase plugin goal
clean maven-clean-plugin clean
process-resources maven-resources-plugin resources
compile maven-resources-plugin compile
process-test-resources maven-resources-plugin testResources
test-compile maven-compiler-plugin testCompile
test maven-compiler-plugin test
install maven-install-plugin install

We are going to progressively add build functionnalities around this default execution. It will help you understand better the generated artifacts configuration.

4 – Filter files

Filtering resources is a very important maven feature. It allows per environment resources customization.
Google provides a MapView UI component that displays and animates a map. That component requires a generated key based on a signature fingerprint.
An Android package (apk file) is always signed, either by a debug key (generated by the sdk) or a proprietary key generated by the user.
As the signing caracteristics are not the same the finger print will differ and the generated map key will also differ. We need to:
– get a map key per env
– inject the key that correspond to the signing caracteristics

Signing is an entire topic but I will broadly present what is required to sign and obtain a map key:
– first create a key

$ keytool -genkey -v -keystore ~/.android/android-from-scratch.keystore -alias android-from-scratch -keyalg RSA -keysize 2048 -validity 10000 -keypass secret -storepass secret

– answer the questions
– get its the finger print

$ keytool -list -keystore ~/.android/android-from-scratch.keystore -alias android-from-scratch -keypass secret -storepass secret
android-from-scratch, 25 mai 2012, PrivateKeyEntry, 
Empreinte du certificat (MD5) : 6F:8E:92:F1:EA:61:FE:B5:FB:60:E7:67:CF:10:F6:F9

– enter ‘6F:8E:92:F1:EA:61:FE:B5:FB:60:E7:67:CF:10:F6:F9’ in this form, accept usage terms and validate form
– copy the obtained key in your MapView component

That was the classic way but when you have multiple map keys you must filter the MapView component:

– declare input resources and target directory

        <resources>
            <resource>
                <directory>${project.basedir}/res</directory>
                <filtering>true</filtering>
                <targetPath>${project.build.directory}/filtered-res</targetPath>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>${project.basedir}/res</directory>
                <filtering>false</filtering>
                <targetPath>${project.build.directory}/filtered-res</targetPath>
                <excludes>
                    <exclude>**/*.xml</exclude>
                </excludes>
            </resource>
        </resources>

– bind filtering process to initialize phase: this was the tricky part because filtering usually occurs at process-resources phase. Why triggering at initialize phase? Well because Android generates java sources (generate-sources phase) based on informations that are in res/folder. The generate-source phase occurs before the process-resources phase so filtering after generation will have no effect. It took me some time to figure out that one.

                      <plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<executions>
					<execution>
						<phase>initialize</phase>
						<goals>
							<goal>resources</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

– don’t forget to notify android-maven-plugin that the default target directory has changed

			<plugin>
				<groupId>com.jayway.maven.plugins.android.generation2</groupId>
				<artifactId>android-maven-plugin</artifactId>
				<inherited>true</inherited>
				<extensions>true</extensions>
				<configuration>
					<resourceDirectory>${project.build.directory}/filtered-res</resourceDirectory>
				</configuration>
			</plugin>

– use place holder

<com.google.android.maps.MapView
xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/map"
android:layout_width="fill_parent" android:layout_height="380px"
android:layout_alignParentLeft="true" android:layout_below="@id/header"
android:apiKey="${sign.maps.key}"
android:clickable="true" />

– provide value for that place holder

<profiles>
<profile>
<id>android-debug</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<sign.maps.key>0wGHQZmAJv4d-W3pP_37e_LwNrifmfI8j4P-okw</sign.maps.key>
</properties>
</profile>
</profiles>

That’s it for filtering. we can focus on a custom signing process

5 – Sign with your own caracteristics

In debug mode signing does occur but you don’t have anything to configure, the platform (called under the hood by the plugin) does it for you. It signs with defaults (as stated here):

Keystore name: "debug.keystore"
Keystore password: "android"
Key alias: "androiddebugkey"
Key password: "android"
CN: "CN=Android Debug,O=Android,C=US"

The key is stored under ~/.android.debug.keystore

When you don’t want to use the debug key you must:
– disable the debug signing in the plugin (your artifact is not signed any more)

			<plugin>
				<groupId>com.jayway.maven.plugins.android.generation2</groupId>
				<artifactId>android-maven-plugin</artifactId>
				<inherited>true</inherited>
				<extensions>true</extensions>
				<configuration>
					<sign>
						<debug>false</debug>
					</sign>
				</configuration>
			</plugin>

– declare the maven-jarsigner-plugin

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jarsigner-plugin</artifactId>
				<executions>
					<execution>
						<id>signing</id>
						<phase>package</phase>
						<goals>
							<goal>sign</goal>
							<goal>verify</goal>
						</goals>
						<inherited>true</inherited>
						<configuration>
							<removeExistingSignatures>true</removeExistingSignatures>
							<archiveDirectory />
							<includes>
								<include>${project.build.directory}/${project.artifactId}.apk</include>
							</includes>
							<keystore>${sign.keystore}</keystore>
							<alias>${sign.alias}</alias>
							<storepass>${sign.storepass}</storepass>
							<keypass>${sign.keypass}</keypass>
							<verbose>true</verbose>
						</configuration>
					</execution>
				</executions>
			</plugin>

This plugin will be called during the package phase after the android-maven-plugin:apk goal
– set signing properties. If these are sensible values (as it should be for production) put the below section in ~/.m2/settings.xml

	<profiles>
		<profile>
			<id>android-prepare-release</id>
			<properties>
				<sign.keystore>~/.android/android-from-scratch.keystore</sign.keystore>
				<sign.alias>android-from-scratch</sign.alias>
				<sign.keypass>******</sign.keypass>
				<sign.storepass>******</sign.storepass>
				<sign.maps.key>0wGHQZmAJv4d-W3pP_37e_LwNrifmfI8j4P-okw</sign.maps.key>
			</properties>
		</profile>
	</profiles>

6 – Activate obfuscation

The default android-maven-plugin configuration runs obfuscation with proguard
You can enable or disable it. The proguard.cfg configures its behaviour

			<plugin>
				<groupId>com.jayway.maven.plugins.android.generation2</groupId>
				<artifactId>android-maven-plugin</artifactId>
				<inherited>true</inherited>
				<extensions>true</extensions>
				<configuration>
					<proguard>
						<skip>false</skip>
					</proguard>
				</configuration>
			</plugin>

7 – Control zipalign

The zipalign goal is not bound by default. You have to bind it to the package phase (after signature verification). You can specify zipalign properties in the config section of the plugin

			<plugin>
				<groupId>com.jayway.maven.plugins.android.generation2</groupId>
				<artifactId>android-maven-plugin</artifactId>
				<inherited>true</inherited>
				<extensions>true</extensions>
				<configuration>
                                        <zipalign>
						<verbose>true</verbose>
						<inputApk>${project.build.directory}/${project.artifactId}.apk</inputApk>
						<outputApk>${project.build.directory}/${project.artifactId}-signed-aligned.apk</outputApk>
					</zipalign>
				</configuration>
				<executions>
					<execution>
						<id>alignApk</id>
						<phase>package</phase>
						<goals>
							<goal>zipalign</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

8 – Attach apk artifact

The apk artifact will normally not be deployed to repositories (local or remote) because it has a custom extension. The build-helper plugin helps in declaring that artifact as ‘shippable’ for distribution

			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>build-helper-maven-plugin</artifactId>
				<configuration>
					<artifacts>
						<artifact>
							<file>${project.build.directory}/${project.artifactId}-signed-aligned.apk</file>
							<type>apk</type>
							<classifier>signed-aligned</classifier>
						</artifact>
					</artifacts>
				</configuration>
				<executions>
					<execution>
						<id>attach-signed-aligned</id>
						<phase>package</phase>
						<goals>
							<goal>attach-artifact</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

9 – Run a device

9.1 – Emulated device

– create the device if needed: cf previous post
– run the device: cf previous post

9.2 – Hardware device

– plug your device via USB cable
– allow USB debugging on your device (usually found on Settings->Application->Debug)
– configure your device on your computer to avoid “??????????????? no permissions” (if needed)

$ adb devices
List of devices attached
???????????? no permissions

Find your device vendror id

$ lsusb
Bus 001 Device 004: ID 04e8:6860 Samsung Electronics Co., Ltd

Add it to the device rules files if not present

$ echo '' >>  /etc/udev/rules.d/51-android.rules
$ echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="04e8", MODE="0666"' >>  /etc/udev/rules.d/51-android.rules
$ sudo service udev restart
$ sudo killall adb
$ sudo adb start-server

More on that in this post

Unplugging and plugging again should now display your device serial number (the one you use in adb -s ….) when running

$ adb devices
List of devices attached
00192efc152b7e	device

10 – Run your build

Some dev clycles later you are ready to build. Just run the below command and it will run the almost full execution (release will be missing because I do not have a continuous integration platform at home :)).
mvn clean install (-Pandroid-debug) or mvn clean install -Pandroid-release-prepare

You should have this new execution schedule

maven phase plugin goal
clean maven-clean-plugin clean
generate-sources android-maven-plugin generate-sources
process-resources maven-resources-plugin resources
process-resources android-maven-plugin manifest-update
compile maven-resources-plugin compile
process-classes android-maven-plugin proguard
process-test-resources maven-resources-plugin testResources
test-compile maven-compiler-plugin testCompile
test maven-compiler-plugin test
prepare-package android-maven-plugin emma
prepare-package android-maven-plugin dex
package maven-jar-plugin jar
package android-maven-plugin apk
package android-maven-plugin sign
package android-maven-plugin verify
package android-maven-plugin zipalign
package build-helper-maven-plugin attach-artifact
pre-integration-test android-maven-plugin internal-pre-integration-test
integration-test android-maven-plugin internal-integration-test
install maven-install-plugin install

11 – Release/Publish

11.1 – Release

You can release without deploying to a remote repository. You still need to declare your scm settings.

<scm>
<connection>scm:git:git@github.com:lgueye/android-from-scratch-parent.git</connection>
<url>scm:git:git@github.com:lgueye/android-from-scratch-parent.git</url>
</scm>

You can then run maven release plugin

$ mvn release:prepare -DpreparationGoals='clean install -Pandroid-release-prepare -Dandroid.device=00192efc152b7e' --batch-mode

It will change pom versions to release version, commit, create tag, checkout tag, change pom versions to new development version and commit.

11.2 – Publish

I’ve never published on Google Play because of the branding part. The publish platform requires a lot of brandings that I didn’t provide because I suck at marketing my products: I’ve never resized any picture with Gimp or Photoshop. I’m just not interested in that kind of knowledge.
Anyway to publish you must subscribe an Android Developer account which will cost you 25 USD. Then you’ll have access to the Android Developer Console (web) that invite you to fill a hundred fields related to the publishing content: titles, descriptions, various picture size for the logo, various application usage picture, etc.
Below some screens of the application branding form:

You can read more on the topic in the official documentation

Well that was quite dense. Sorry if I lost you. The topic is actually quite extended and I tried to remain as precise as possible.
The resulting app is a very rough Google Map app that uses zoom in, zoom out, center and markers. The source code is on github

I would like to thank the Manfred Moser (from Simpligility) and Hugo Josefson (from Jayway) among other contributors. The plugin is not perfect but a lot of work has already been done there. Without that work, Android build would still require a lot of manual execution.

Advertisements

8 thoughts on “Android from scratch, part 2: use android-maven-plugin

  1. Hi !

    Nice post, thanks a lot !

    With maven, is there a way to compact several android maven module into one apk? (so each module has it’s own version and user see only one app installed on his device)

    1. Hi,

      Thank you, glad to help.
      I’m not sure of what you’re trying to achieve but I think it’s possible. If you want custom assembly you should explore maven-assembly plugin.
      But I’d rather transform the modules into dependencies and ship them in the final artifact.
      I strongly encourage you to subscribe the Maven Android Developers group. I think this post might help.

      Good Luck!

  2. Hi,
    I am trying to use maven android plugin 3.6.0. When I generate my apk file it is generating only with resources, it does not contain any .class files. I tried a lot to research on this but i am not able find any solution for this. This even happens for simple sample project.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s