Automatic Android obfuscated build
Posted by Tonio | Filed under Tutorial
In the last release of it’sĀ Android SDK, Google bundled the ProGuard utility for code obfuscation and optimisation. This integration makes it easy to automate the process of building optimized applications for the Android Market. Let’s see how it is possible to use Ant to maintain one source code only and be able to generate two distinct optimized, signed and obfuscated apk that provide differents functionnalities.
The problem
Take the example of my bubble level application. This application is available in two versions on the Android Market. A free (ad-free) and a paid version for those who wish to make a small donation to the developper !
The only difference between the two applications is the presence or absence of links “Donate” and “My Applications” in the preferences.
In order not to complicate the development, we wish to have only a single version of the code source to maintain and an Ant script to generate the two different apk for the Android Market.
The Ant script
Be sure you have the version 8 (or more) of the Android SDK installed. To generate the scripts skeleton to automate the build of the application, run the following command from the tools directory of the Android SDK installation :
dir %ANDROID_HOME%/tools android update project --path ./Level
You’ll find three additional files at the root of your project’s directory :
- build.xml
- Ant script to generate an unsigned, non obfuscate and non-optimized Android apk
- local.properties
- Configuration file for the build.xml Ant script. Use it ti specify properties specific to your computer such as the path to your keystore, the Android SDK installation directory, …
- proguard.cfg
- ProGuard configuration file adapted to Android applications.
To start the build of an unsigned, non obfuscated Android apk just call the release Ant target of the build.xml file :
ant release
To start the build of a signed, non obfuscated Android apk you must add two properties to the local.properties configuration file and call the release Ant target of the build.xml file again :
key.store=/chemin/vers/mon/keystore.ks key.alias=alias
In order to build a signed and obfuscated Android apk,the last thing to do is to configure the build.xml Ant file to use ProGuard. To achieve this, just create a build.properties file that points to the ProGuard configuration file and call the release Ant target of the build.xml file againĀ :
proguard.config=proguard.cfg
It is highly important to carefully re-test the application before publishing it in an obfuscated version. Indeed, the AndroidManifest.xml file registers class names and methods names that would be modified by ProGuard. Fortunately the default proguard.cfg file generated by the Android SDK preserves the code that is referenced by the file AndroidManifest.xml. This is not the case for the click listeners introduced version 1.6. If you register a click listener for Button directly in the layout file where the Button is declared, the default ProGuard configuration file will not preserve the method name in the Activity that uses the layout:
<button android:onclick="monClickListener"/>I recommend you not to declare click listeners for any Button in the layout files of your applications. You can also read the ProGuard documentation and find a way for keeping the click listeners names.
Code preparation
We will now discuss how to maintain two different versions of an application through a single source. The example is simple, the solution will also be. In order to retain preferences “Donate” and “My Applications” only for the free application, we will modify the PreferenceActivity LevelPreferences.java by adding true or false conditions around the code that distinguishes the two versions :
package net.androgames.level; import net.androgames.level.config.DisplayType; import net.androgames.level.config.Provider; import net.androgames.level.config.Viscosity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; public class LevelPreferences extends PreferenceActivity implements OnPreferenceChangeListener { public static final String KEY_SHOW_ANGLE = "preference_show_angle"; public static final String KEY_DISPLAY_TYPE = "preference_display_type"; public static final String KEY_SOUND = "preference_sound"; public static final String KEY_LOCK = "preference_lock"; public static final String KEY_LOCK_LOCKED = "preference_lock_locked"; public static final String KEY_LOCK_ORIENTATION = "preference_lock_orientation"; public static final String KEY_APPS = "preference_apps"; public static final String KEY_DONATE = "preference_donate"; public static final String KEY_SENSOR = "preference_sensor"; public static final String KEY_VISCOSITY = "preference_viscosity"; public static final String KEY_ECONOMY = "preference_economy"; private static final String PUB_APPS = "market://search?q=pub:\"Antoine Vianey\""; private static final String PUB_DONATE = "market://details?id=net.androgames.level.donate"; private static final int DIALOG_CALIBRATE_AGAIN = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); if (true) { PreferenceCategory appsCategory = new PreferenceCategory(this); appsCategory.setTitle(R.string.preference_apps_category); Preference appsPreference = new Preference(this); appsPreference.setTitle(R.string.preference_apps); appsPreference.setSummary(R.string.preference_apps_summary); appsPreference.setKey(KEY_APPS); Preference donatePreference = new Preference(this); donatePreference.setTitle(R.string.preference_donate); donatePreference.setSummary(R.string.preference_donate_summary); donatePreference.setKey(KEY_DONATE); getPreferenceScreen().addPreference(appsCategory); appsCategory.addPreference(donatePreference); appsCategory.addPreference(appsPreference); } } public void onResume() { super.onResume(); // enregistrement des listerners findPreference(KEY_DISPLAY_TYPE).setOnPreferenceChangeListener(this); findPreference(KEY_SENSOR).setOnPreferenceChangeListener(this); findPreference(KEY_VISCOSITY).setOnPreferenceChangeListener(this); findPreference(KEY_ECONOMY).setOnPreferenceChangeListener(this); // mise a jour de l'affichage SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(this); findPreference(KEY_DISPLAY_TYPE).setSummary(DisplayType.valueOf( prefs.getString(LevelPreferences.KEY_DISPLAY_TYPE, "ANGLE")) .getSummary()); findPreference(KEY_SENSOR).setSummary(Provider.valueOf( prefs.getString(LevelPreferences.KEY_SENSOR, "ORIENTATION")) .getSummary()); findPreference(KEY_VISCOSITY).setSummary(Viscosity.valueOf( prefs.getString(LevelPreferences.KEY_VISCOSITY, "MEDIUM")) .getSummary()); findPreference(KEY_VISCOSITY).setEnabled( !((CheckBoxPreference) findPreference(KEY_ECONOMY)) .isChecked()); if (true) { // lancement du market findPreference(KEY_APPS).setOnPreferenceClickListener( new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(PUB_APPS)); LevelPreferences.this.startActivity(intent); return true; } }); findPreference(KEY_DONATE).setOnPreferenceClickListener( new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(PUB_DONATE)); try { LevelPreferences.this.startActivity(intent); } catch (ActivityNotFoundException anfe) {} return true; } }); } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { String key = preference.getKey(); if (KEY_DISPLAY_TYPE.equals(key)) { preference.setSummary(DisplayType.valueOf((String) newValue) .getSummary()); } else if (KEY_SENSOR.equals(key)) { preference.setSummary(Provider.valueOf((String) newValue) .getSummary()); showDialog(DIALOG_CALIBRATE_AGAIN); } else if (KEY_VISCOSITY.equals(key)) { preference.setSummary(Viscosity.valueOf((String) newValue) .getSummary()); } else if (KEY_ECONOMY.equals(key)) { findPreference(KEY_VISCOSITY).setEnabled(!((Boolean) newValue)); } return true; } protected Dialog onCreateDialog(int id) { Dialog dialog; switch(id) { case DIALOG_CALIBRATE_AGAIN: AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.calibrate_again_title) .setIcon(android.R.drawable.ic_dialog_alert) .setCancelable(true) .setNegativeButton(R.string.ok, new DialogInterface .OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }) .setMessage(R.string.calibrate_again_message); dialog = builder.create(); break; default: dialog = null; } return dialog; } }
The if (true) {…} and if (false) {…} directives will be automatically optimized by ProGuard during the optimization phase of the build : the unavailable or unused code blocs are automatically removed by the tool.
The last step is to create an Ant script to replace the if (true) directives by if (false) and run the ant task release on both versions of the sources : the original one and the modified one.
The final Ant script
Each application of the Android Market must declare a unique package in its AndroidManifest.xml file. To publish two different versions, it is necessary to change the declared package name. In our case, we’ll use :
- net.androgames.level : pour la version gratuite
- net.androgames.level.donate : pour la version payante
The renaming of the package is not without consequence, since it involves moving the main activity (and other activities, receivers, and services declared in the AndroidManifest.xml) into the new package, and to amend the packages and imports statements for these classes and the referent classes.
Imagine that we maintain the free version of the sources with the use of additional preferences. Initially, we will duplicate the entire project to a temporary working directory :
<mkdir dir="${temp.dir}" /> <copy todir="${temp.dir}"> <fileset dir="."> <exclude name="**/${temp.dir}/**" /> </fileset> </copy>
We will then have to make the necessary changes to the package names, imports and to replace the if (true) directive by if (false) :
<replace file="${temp.dir}/AndroidManifest.xml" token="net.androgames.level" value="net.androgames.level.donate"/> <replace dir="${temp.dir}" value="net.androgames.level.donate.R"> <include name="**/*.java"/> <replacetoken><![CDATA[net.androgames.level.R]]></replacetoken> </replace> <replace dir="${temp.dir}" value="net.androgames.level.donate.Level"> <include name="**/*.java"/> <replacetoken><![CDATA[net.androgames.level.Level]]></replacetoken> </replace> <move todir="${temp.dir}/src/net/androgames/level/donate"> <fileset dir="${temp.dir}/src/net/androgames/level"> <include name="*.java"/> </fileset> </move> <replace dir="${temp.dir}" value="package net.androgames.level.donate;"> <include name="**/*.java"/> <replacetoken><![CDATA[package net.androgames.level;]]></replacetoken> </replace> <replace file="${temp.dir}/src/net/androgames/level/donate/LevelPreferences.java"> <replacetoken><![CDATA[if (true) {]]></replacetoken> <replacevalue><![CDATA[if (false) {]]></replacevalue> </replace>
Once the sources modified in the temporary directory, it may be necessary to delete the directory containing the generated java files of the gen directory so that they will be regenerated during the construction of the apk. If the alternative version contains various resources (strings, arrays, attrs …), the regeneration is essential to prevent numerous RuntimeException.
It only remains to put everything in a single Ant script :
<?xml version="1.0" encoding="iso-8859-1"?> <project name="Level build" default="all"> <property file="make.properties" /> <target name="all" description="Package the two versions" depends="prepare-livraison, do-original, do-modified, clean"/> <target name="prepare-livraison" depends="clean"> <mkdir dir="${temp.dir}" /> <copy todir="${temp.dir}"> <fileset dir="."> <exclude name="**/${temp.dir}/**" /> </fileset> </copy> <replace file="${temp.dir}/build.xml" token="${original.name}" value="${modified.name}"/> <replace file="${temp.dir}/AndroidManifest.xml" token="net.androgames.level" value="net.androgames.level.donate"/> <replace dir="${temp.dir}" value="net.androgames.level.donate.R"> <include name="**/*.java"/> <replacetoken><![CDATA[net.androgames.level.R]]></replacetoken> </replace> <replace dir="${temp.dir}" value="net.androgames.level.donate.Level"> <include name="**/*.java"/> <replacetoken><![CDATA[net.androgames.level.Level]]></replacetoken> </replace> <move todir="${temp.dir}/src/net/androgames/level/donate"> <fileset dir="${temp.dir}/src/net/androgames/level"> <include name="*.java"/> </fileset> </move> <replace dir="${temp.dir}" value="package net.androgames.level.donate;"> <include name="**/*.java"/> <replacetoken> <![CDATA[package net.androgames.level;]]> </replacetoken> </replace> <replace file="${temp.dir}/src/net/androgames/level/donate/LevelPreferences.java"> <replacetoken><![CDATA[if (true) {]]></replacetoken> <replacevalue><![CDATA[if (false) {]]></replacevalue> </replace> </target> <target name="clean"> <delete dir="${temp.dir}"/> </target> <target name="do-original"> <echo message="Original version" /> <ant dir="." antfile="build.xml" target="release"/> </target> <target name="do-modified"> <echo message="Alternative version" /> <delete dir="${temp.dir}/gen" /> <ant dir="${temp.dir}" antfile="build.xml" target="release"/> <move file="${temp.dir}/bin/${modified.name}-release.apk" todir="bin"/> </target> </project>
With it’s own configuration file :
original.name=Level
modified.name=Level-donate
temp.dir=tmpYou can see two signed, obfuscated and optimized apk in the bin directory of the application project :
Tags: Ant, Obfuscation, ProGuard





