Tuesday, October 11, 2011

Including additional javax.* packages in your Android App


Android is missing some APIs.

Android does not include a full set of javax packages. These APIs have been omitted for various reasons:
  • To keep the system small. Including more APIs consumes more of a device's limited disk.
  • To avoid versioning problems. APIs included in the core platform are frozen at a particular release version. This limits developers' ability to choose the version that's right for their application.
Although many javax.* APIs aren't included in the core platform, application developers can still use the APIs by repackaging them. Repackaging involves moving classes from the protected javax.* namespace into an application namespace like com.mycompany.*.
This guide describes how to repackage an API. It assumes you are using Ant to build your application.
Complete source code for the finished example used in this guide is available here. You can check out and build a copy with:
svn checkout http://dalvik.googlecode.com/svn/trunk/examples/hello_stax hello_stax
cd hello_stax
android update project -p .
ant debug

Copy Jars

We need the .jar files that contain the code of interest. Many JSRs offer code for download on java.net or code.google.com. Copy the .jarfiles to the project's libs directory. At this point, building the project should fail with an error:
$ ant installBuildfile: build.xml
    [setup] Project Target: Android 2.0.1
    [setup] API level: 6

    ...
-dex:
     [echo] Converting compiled files and external libraries into /home/jessewilson/svn/dalvik/examples/hello_stax/bin/classes.dex...
     [echo]          
    [apply] 
    [apply] trouble processing "javax/xml/namespace/NamespaceContext.class":
    [apply] 
    [apply] Attempt to include a core class (java.* or javax.*) in something other
    [apply] than a core library. It is likely that you have attempted to include
    [apply] in an application the core library (or a part thereof) from a desktop
    [apply] virtual machine. This will most assuredly not work. At a minimum, it
    [apply] jeopardizes the compatibility of your app with future versions of the
    [apply] platform. It is also often of questionable legality.
    [apply] 
    [apply] If you really intend to build a core library -- which is only
    [apply] appropriate as part of creating a full virtual machine distribution,
    [apply] as opposed to compiling an application -- then use the
    [apply] "--core-library" option to suppress this error message.
    [apply] 
    [apply] If you go ahead and use "--core-library" but are in fact building an
    [apply] application, then be forewarned that your application will still fail
    [apply] to build or run, at some point. Please be prepared for angry customers
    [apply] who find, for example, that your application ceases to function once
    [apply] they upgrade their operating system. You will be to blame for this
    [apply] problem.
    [apply] 
    [apply] If you are legitimately using some code that happens to be in a core
    [apply] package, then the easiest safe alternative you have is to repackage
    [apply] that code. That is, move the classes in question into your own package
    [apply] namespace. This means that they will never be in conflict with core
    [apply] system classes. If you find that you cannot do this, then that is an
    [apply] indication that the path you are on will ultimately lead to pain,
    [apply] suffering, grief, and lamentation.
    [apply] 
    [apply] 1 error; aborting

BUILD FAILED/home/jessewilson/android-sdk-linux_86/platforms/android-2.0.1/templates/android_rules.xml:259: The following error occurred while executing this line:
/home/jessewilson/android-sdk-linux_86/platforms/android-2.0.1/templates/android_rules.xml:123: apply returned: 1

Customize the ant build.xml

Before we can fix the above problem, we need to customize our build file. Android's default build.xml file imports build rules from a standard set. Since including a javax.* package isn't supported by the standard rules, we need to copy those rules into a place where we can modify them.
The standard rules are imported from
<SDK>/platforms/<target_platform>/templates/android_rules.xml
To customize some build steps for your project:
  1. copy the content of the main node <project> from android_rules.xml
  2. paste it in your build.xml below the <setup /> task.
  3. disable the import by changing the <setup /> task to <setup import="false" />
This ensures that the properties are setup correctly but that the customized build steps are used.

Repackage the code with jarjar

Jar Jar Links is an easy-to-use repackager that integrates nicely with Ant. Download jarjar-1.0.jar from the project site. Save the file in a new project subdirectory, buildtools.
Create a jarjar target in the build.xml file.
    <!-- Converts this project's .class files into .dex files -->
    <target name="-jarjar" depends="compile">
        <taskdef name="jarjar" classname="com.tonicsystems.jarjar.JarJarTask"
                 classpath="buildtools/jarjar-1.0.jar"/>
        <jarjar jarfile="${out.absolute.dir}/repackagedclasses.jar">
            <fileset dir="${out.classes.absolute.dir}" />
            <zipgroupfileset dir="${external.libs.absolute.dir}" includes="*.jar" />
            <rule pattern="javax.xml.**" result="com.mycompany.@1"/>
        </jarjar>
    </target>
At this point you'll need to edit the <rule /> tag to refer to the javax.* appropriate package prefix, and to the rename target. The jarjar Getting Started Guide explains the syntax.
This task builds a jar that contains all of the code, including repackaged library code and application code. We need turn those classes into a Dalvik executable. Edit the dex-helper macro definition to point at the repackaged code. We replace the tags:
             <arg path="${out.classes.absolute.dir}" />
             <fileset dir="${external.libs.absolute.dir}" includes="*.jar" />
with the repackaged classes:
             <fileset file="${out.absolute.dir}/repackagedclasses.jar" />
The complete macro should look something like this:
    <!-- Configurable macro, which allows to pass as parameters output directory,
         output dex filename and external libraries to dex (optional) -->
    <macrodef name="dex-helper">
       <element name="external-libs" optional="yes" />
       <element name="extra-parameters" optional="yes" />
       <sequential>
         <echo>Converting compiled files and external libraries into ${intermediate.dex.file}...
         </echo>
         <apply executable="${dx}" failonerror="true" parallel="true">
             <arg value="--dex" />
             <arg value="--output=${intermediate.dex.file}" />
             <extra-parameters />
             <arg line="${verbose.option}" />
             <fileset file="${out.absolute.dir}/repackagedclasses.jar" />
             <external-libs />
         </apply>
       </sequential>
    </macrodef>
Finally, change the -dex target to depend on our new -jarjar target instead of the compile target:
    <target name="-dex" depends="-jarjar">
        <dex-helper />
    </target>

Try it out

Use adb logcat to monitor exceptions as you launch the application. For many javax.* packages, these steps above will be sufficient.

Factory Registration

Some packages will fail to initialize due to factory registration problems. This is common in libraries that support pluggable implementations:
D/AndroidRuntime(  557): Shutting down VM
W/dalvikvm(  557): threadid=3: thread exiting with uncaught exception (group=0x4001b188)
E/AndroidRuntime(  557): Uncaught handler: thread main exiting due to uncaught exception
E/AndroidRuntime(  557): com.mycompany.stream.FactoryConfigurationError: Provider com.bea.xml.stream.MXParserFactory not found
E/AndroidRuntime(  557):        at com.mycompany.stream.FactoryFinder.newInstance(FactoryFinder.java:72)
E/AndroidRuntime(  557):        at com.mycompany.stream.FactoryFinder.find(FactoryFinder.java:176)
E/AndroidRuntime(  557):        at com.mycompany.stream.FactoryFinder.find(FactoryFinder.java:92)
E/AndroidRuntime(  557):        at com.mycompany.stream.XMLInputFactory.newInstance(XMLInputFactory.java:136)
E/AndroidRuntime(  557):        at com.googlecode.dalvik.stax.HelloStax.onCreate(HelloStax.java:39)
E/AndroidRuntime(  557):        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
E/AndroidRuntime(  557):        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2444)
E/AndroidRuntime(  557):        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2497)
E/AndroidRuntime(  557):        at android.app.ActivityThread.access$2200(ActivityThread.java:119)
E/AndroidRuntime(  557):        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1848)
E/AndroidRuntime(  557):        at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(  557):        at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(  557):        at android.app.ActivityThread.main(ActivityThread.java:4338)
E/AndroidRuntime(  557):        at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(  557):        at java.lang.reflect.Method.invoke(Method.java:521)
E/AndroidRuntime(  557):        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
E/AndroidRuntime(  557):        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
E/AndroidRuntime(  557):        at dalvik.system.NativeStart.main(Native Method)
The solution is to add system properties that tell the API where to find its implementation. The properties below configure Sun's implementation of StAX; you will need different properties for different APIs. These properties and their values are usually obtained from the META-INF/services/ directory inside your implementation's .jar file:
public class HelloStax extends Activity {
  @Override public void onCreate(Bundle savedInstanceState) {
    /*
     * Configure which implementation of StAX that will be used by our
     * application. These properties and their values are obtained from the
     * META-INF/services/ directory inside the implementation .jar file.
     */
    System.setProperty("javax.xml.stream.XMLInputFactory",
        "com.sun.xml.stream.ZephyrParserFactory");
    System.setProperty("javax.xml.stream.XMLOutputFactory",
        "com.sun.xml.stream.ZephyrWriterFactory");
    System.setProperty("javax.xml.stream.XMLEventFactory",
        "com.sun.xml.stream.events.ZephyrEventFactory");

    ...
  }
}

Factory Class Loaders

After setting system properties to specify factory implementations, factory loading may fail due to a class not found problem:
E/AndroidRuntime(  575): com.mycompany.stream.FactoryConfigurationError: Provider com.sun.xml.stream.ZephyrParserFactory not found
E/AndroidRuntime(  575):        at com.mycompany.stream.FactoryFinder.newInstance(FactoryFinder.java:72)
E/AndroidRuntime(  575):        at com.mycompany.stream.FactoryFinder.find(FactoryFinder.java:120)
E/AndroidRuntime(  575):        at com.mycompany.stream.FactoryFinder.find(FactoryFinder.java:92)
E/AndroidRuntime(  575):        at com.mycompany.stream.XMLInputFactory.newInstance(XMLInputFactory.java:136)
E/AndroidRuntime(  575):        at com.googlecode.dalvik.stax.HelloStax.onCreate(HelloStax.java:39)
E/AndroidRuntime(  575):        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
E/AndroidRuntime(  575):        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2444)
E/AndroidRuntime(  575):        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2497)
E/AndroidRuntime(  575):        at android.app.ActivityThread.access$2200(ActivityThread.java:119)
E/AndroidRuntime(  575):        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1848)
E/AndroidRuntime(  575):        at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(  575):        at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(  575):        at android.app.ActivityThread.main(ActivityThread.java:4338)
E/AndroidRuntime(  575):        at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(  575):        at java.lang.reflect.Method.invoke(Method.java:521)
E/AndroidRuntime(  575):        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
E/AndroidRuntime(  575):        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
E/AndroidRuntime(  575):        at dalvik.system.NativeStart.main(Native Method)
The fix here is a cumbersome one. We need to manually set the class loader that will be used by the factory to the application's class loader.
public class HelloStax extends Activity {
  @Override public void onCreate(Bundle savedInstanceState) {

    ...

    /*
     * Ensure the factory implementation is loaded from the application
     * classpath (which contains the implementation classes), rather than the
     * system classpath (which doesn't).
     */
    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

    ...
  }
}

Success

At this point, we have repackaged the javax.* package in order to include it in our application. The package will continue to work, even if future versions of Android include a conflicting version of the API.
Comment by kevin.t....@gmail.comJan 15, 2010
Note that in the repackaging step, <rule pattern="javax.xml.**" result="com.mycompany.@1"/> repackages everything in javax.xml, so your call to JarJar? had better include JARs with all of the javax.xml classes you use. For example, javax.xml.parsers.DocumentBuilderFactory? is available in Android, and is not part of the StAX API or StAX implementation. If the user code uses javax.xml.parsers.DocumentBuilderFactory?, it will be changed by JarJar? to use com.mycompany.parsers.DocumentBuilderFactory?, which won't be present (assuming your didn't JarJar? in some jar that had that class).
Comment by kevin.t....@gmail.comJan 18, 2010
Shouldn't Android be invoking the call to Thread.setContextClassLoader when it creates the thread?
Comment by hindenb...@gmail.comFeb 4, 2010
jarjar does not work with ant 1.8 RC1. This caused the example to fail and it was fairly confusing. Use ant 1.7.1
When I run this example I see this output in my logcat: VFY: unable to find class referenced in signature (Lcom/mycompany/transform/Source;
However it still manages to run and I see the xml output in my log cat.
If I try to let eclipse compile the example, I get a compile error: javax.xml.transform.Source cannot be resolved. It is indirectly referenced from required .class files
I was kind of ok with moving to an ant build for my application, but Eclipse really needs to be able to compile things in order for this to be a feasible strategy.
Source : http://code.google.com/p/dalvik/wiki/JavaxPackages

No comments:

Post a Comment