Wednesday, December 14, 2011

Exception reporting in Android applications

I was surprised and delighted to discover that the Android Market infrastructure has a mechanism to capture and report program exceptions which occur on the mobile device.  I discovered this because my app had a bug, and this system told me exactly where it was.

When a crash happens in the Android java code, it appears that either the Dalvik JVM and/or some monitor program in the underlying linux operating system captures the stack trace, uploads it to Google, and presents it to the app developer on the Developer Dashboard.  (Click here to go to yours.)

Early last week, I was trolling for statistics when I spotted this on my Developer Dashboard.  It reported one error and one report.  Within a few days, it had jumped to five reports.  (disclaimer: some image fields grayed-out for privacy)



I clicked on the circled link, and the next page summarized the problem, class name, and method name.



I clicked on the 'NullPointerException' link and the next page showed the exact line number.   Very nice.




I debugged the problem.  That line in my source code was not expecting or handling a null input parameter.  So when it got called with null, the app died.  My bad.  But easy to fix.

After I fixed the problem and udated the app version at the Android Market, I went back to the error page and clicked the button to mark this as an 'old' problem.  This seems to have moved the problem lower in my view.



I hope all my code bugs are this easy to fix.  Good luck debugging yours.

Tuesday, December 13, 2011

How to sign an APK for the Android Market


In order to publish an application at the Android Market, the app must be 'signed' with your own  'certificate'.  This tutorial explains how to manually create a certificate and sign the app.

Important:  Future updates and fixes to the app must also be signed with the original certificate in order to maintain the identity of the app.  Therefore, you must archive the certificate and its passwords in a safe place for the lifetime of the application.

This tutorial was developed using Eclipse 3.6.2 and command-line tools on Ubuntu Linux 10.04.  Other platforms are similar.

Prereqs

- Create an Android app using the Eclipse IDE and Android SDK.

- Have command-line tool 'keytool' from the Java JRE in your path.
    example:  /opt/jdk1.6.0_25/bin/keytool

- Have command-line tool 'jarsigner' from the Java JRE in your path.
    example:  /opt/jdk1.6.0_25/bin/jarsigner

- Have command-line tool 'zipalign' from the Android SDK in your path.
    example:  /opt/android-sdk-linux/tools/zipalign

- Recommended: Create directories for release artifacts.   examples:
    /home/sag/android/releases
    /home/sag/android/certificates

Export unsigned APK from Eclipse

From the Package View in Eclipse, right-click the project name-> Android Tools-> Export unsigned application package

Recommended: Specify a filename with 'raw' in the name.
    example:  /home/sag/android/releases/myapp.raw.apk






Optional: Make a backup copy.
    example:  cp  myapp.raw.apk  myapp.raw.apk.bkp

Create certificate

Note: Multiple certificates may be stored in one container file called a 'keystore database'.  This tutorial creates exactly one certificate.

Use the 'keytool' command to create a keystore file containing one certificate.

keytool -genkey
        -v
        -keystore "/home/sag/android/certificates/myalias.keystore"
        -alias myalias
        -storepass xxxxx
        -keypass yyyyy
        -keyalg RSA
        -validity 10000

where myalias is an arbitrary name, and storepass and keypass are passwords you memorize and save.


Enter your information when prompted.  examples:
    First and Last Name: John Doe
    Organizational Unit: Android Mobile Development
    Organization: myapp.example.org
    Location: San Francisco
    State: CA
    Country US

Verify a binary file is created, myalias.keystore.  Mine was 1400+ bytes.

Sign APK

Apply the certificate to the unsigned APK.

Note: This step modifies the existing APK file.

jarsigner -keystore "/home/sag/android/certificates/myalias.keystore"
          -storepass xxxxx
          -keypass yyyyy
          "/home/sag/android/releases/myapp.raw.apk"
          myalias

Optional:  Verify the file is similar in size to the original backup copy.


Zipalign APK

Align the APK on multiple-byte boundaries for efficiency.

Note: This step creates a new APK file.

zipalign  -v 4  myapp.raw.apk  myapp.apk

Verify the new aligned APK file is similar in size to the raw APK.

ls -l
-rw-r--r-- 1 sag  sag  64287 2011-12-12 15:36 myapp.raw.apk.bkp
-rw-r--r-- 1 sag  sag  66375 2011-12-12 15:39 myapp.raw.apk
-rw-r--r-- 1 sag  sag  66382 2011-12-12 15:40 myapp.apk

Et voila.  The new file, myapp.apk, is your final APK.  It may be published on the Android Market.


Postscripts

To install the final APK on a device, you must manually uninstall the previous version of app.  This is required because the previous version was signed by a debug certificate which is built into Eclipse and the Android SDK for our convenience.  Also, be sure to tell any beta testers to manually uninstall for the same reason.

As mentioned earlier, you must archive your keystore file (which contains the certificate) and passwords for future use.  This is required in order to publish updates to the app.  If you lose it and are forced to create a new certificate, the Android Market will treat your updates as a new and different app.  Users will not be able to find or install your updates seamlessly.  That would be bad.  Therefore save your certificate and passwords in multiple places.  You've been warned.

It is supposedly possible to sign APKs using a wizard built-into Eclipse.  It is probably easier than this.  Try it once you understand the manual steps outlined here.





Sunday, October 16, 2011

How to send mobile notifications using cURL and C2DM to an Android App


This tutorial explains the simplest possible steps to send notification messages to an Android device using the Google service 'Cloud To Device Messaging', aka 'C2DM'.

This tutorial is organized in the following sections.
1. Architectural Overview:  How C2DM works.
2. Prerequisites:  List of requirements before starting this tutorial.
3. Sign up for C2DM:  This is a human task done with a browser.
4. Android App:  Compile the provided source code into an APK and install it.
5. Generate web requests with cURL.  This simulates the messaging normally implemented in an appserver.
6. Live testing:  Run the app and send notifications.
7. Production notes:  Ideas on building a production-quality app.

Note: Throughout this tutorial, email addresses, passwords, package names, and ID strings are truncated and/or obfuscated for privacy.  You must use your own values.


For more information, see the list of references at the bottom of this article.


1. Architectural Overview

Entitlement:  A person must sign up to use the C2DM service with a Google Mail account.  The java package name of your application is enabled.

Messaging:  Several messages are exchanged between Appserver, Android device, and C2DM to set up notifications.  After that, notification messages may be sent from Appserver to C2DM to device.

The following figure highlights the steps and important data exchanged (click to enlarge):




2. Prerequisites

Before you start, you need the following:
- A Google Mail account.
- A working Android device, version 2.2 or later, with the Android Market installed and account enabled, Internet enabled (I used Wi-Fi only), and Google Talk signed-out.
- Eclipse and the Android SDK installed on your computer.  Know how to compile an APK and install it on your device.  Know how to display log messages using 'adb logcat'.
- cURL installed on your computer.  cURL is a command line tool for sending web requests:  http://curl.haxx.se/


3. Sign up for C2DM

Sign up here:   http://code.google.com/android/c2dm/signup.html 

When I signed up for development purposes, I entered the following:
-  Package name of android app:  <my_package_name>
-  In android market?  No
-  Estimated messages per day:  32
-  Estimated queries per sec:  0-5
-  Additional information:  developer sandbox
-  Contact email: <my_email@gmail.com>
-  Role (sender) account email:  <my_email@gmail.com>
-  Escalation contact:  <my phone number>

The account became active within a few days.


4. Android App

I wrote three extremely simple low-function classes to demonstrate notifications.  All three classes reside in package <my_package_name>.

Copy these three classes into your Eclipse environment with Android SDK.  Edit the package name and email address.  Diff AndroidManifest.xml into yours.  Compile and install the APK on your device.

HelloActivity 

This activity starts the intent which registers the device with C2DM.


package <my_package_name>;


import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;


public class HelloActivity extends Activity {

    /**
     * Hard-coded credentials
     */
    final static String _developerEmail = "<my_email>@gmail.com";

    /**
     * The app starts here.
     * You can view log messages using 'adb logcat'.
     */
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String m = "HelloActivity/onCreate";
        Log.i(m,"Entry.");
        
        TextView tv = new TextView(this);
        tv.setText("Hello, Android");
        setContentView(tv);
        
        // Start an intent to register this app instance with the C2DM service.
  Intent intent = 
            new Intent("com.google.android.c2dm.intent.REGISTER");
  intent.putExtra("app",
            PendingIntent.getBroadcast(this, 0, new Intent(), 0));
        intent.putExtra("sender", _developerEmail);
  startService(intent);
        
        Log.i(m,"Exit.");
    }
}



HelloRegistrationReceiver

This broadcast receiver handles the registration response from C2DM.  It writes the device registration ID from C2DM to log file.

Note: The registration ID string may be several hundred characters in length.


package <my_package_name>;


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;


public class HelloRegistrationReceiver extends BroadcastReceiver {


    /** 
     * Listens for a registration response message from C2DM.
     * Logs the received registration_id string.
     * You can view log messages using 'adb logcat'.
     */
    public void onReceive(Context context, Intent intent) {
String m = "HelloRegistrationReceiver/onReceive";
Log.i(m,"Entry.");

String action = intent.getAction();
Log.i(m,"action=" + action);

if ("com.google.android.c2dm.intent.REGISTRATION".equals(action)) {
  String registrationId = intent.getStringExtra("registration_id");
        Log.i(m,"registrationId=" + registrationId);
        String error = intent.getStringExtra("error");
        Log.i(m,"error=" + error);
    }


     Log.i(m,"Exit.");
    }
}



HelloMessageReceiver

This broadcast receiver handles receipt of notification messages from C2DM.  It writes the message payload to log file.


package <my_package_name>;


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;


public class HelloMessageReceiver extends BroadcastReceiver {


    /** 
     * Listens for a notification message from C2DM.
     * Logs the received message payload.
     * You can view log messages using 'adb logcat'.
     */
    public void onReceive(Context context, Intent intent) {
        String m = "HelloMessageReceiver/onReceive";
        Log.i(m,"Entry.");


        String action = intent.getAction();
        Log.i(m,"action=" + action);


        if ("com.google.android.c2dm.intent.RECEIVE".equals(action)) {
            String payload = intent.getStringExtra("payload");
            Log.i(m,"payload=" + payload);
            String error = intent.getStringExtra("error");
            Log.i(m,"error=" + error);
        }


        Log.i(m,"Exit.");
    }
}



Android Manifest

The following statements for permissions, activity, and receivers were used in this annoying file.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="<my_package_name>"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="8" />


    <!-- Grant permission for this app to use the C2DM service. -->
    <uses-permission android:name="<my_package_name>.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />


    <!-- Prohibit other applications from receiving our notifications. -->
    <permission android:name="<my_package_name>.permission.C2D_MESSAGE"
                android:protectionLevel="signature" />
    
    <application android:icon="@drawable/icon"
                 android:label="@string/app_name">
        <activity android:name=".HelloActivity" 
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <receiver android:name=".HelloRegistrationReceiver" 
                  android:permission="com.google.android.c2dm.permission.SEND">
          <intent-filter>
            <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
            <category android:name="<my_package_name>" />
          </intent-filter>
        </receiver>
        
        <receiver android:name=".HelloMessageReceiver"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="<my_package_name>" />
            </intent-filter>
        </receiver>
        
    </application>
</manifest>



The three classes and Android Manifest complete the application.  Compile it and install it.  Don't start it just yet.

5. Generate web requests with cURL

I used cURL to simulate and fully understand the messages which would normally come from an appserver.  Messages are in the following formats:

Registration request

Sent from cURL to C2DM:

curl -v
     -X POST 
     -d "Email=<my_email>@gmail.com" 
     -d "Passwd=<my_password>"
     -d "source=<my_package_name>" 
     -d "accountType=GOOGLE" 
     -d "service=ac2dm"  
     "https://www.google.com/accounts/ClientLogin"

Registration response

Received from C2DM to cURL.

A successful response is 200 OK.

Three string values are returned in a successful response: 'SID', 'LSID', and 'Auth'.  The last value, 'Auth', is the important token string, <my_server_token>.  It is used subsequently to send notification messages through C2DM to the Android device.

Note: The server token string may be several hundred characters in length.


Notification message

Sent from cURL to C2DM:

curl -v
     -X POST
     -H "Authorization: GoogleLogin auth=<my_server_token>"
     -d "registration_id=<my_device_registration_id>" 
     -d "collapse_key=0"  
     -d "data.payload=<my_notification_payload>"
     "https://android.apis.google.com/c2dm/send"    

Notification response

A successful response is 200 OK.

Aside:  A message ID string is returned in a successful response, in the form  'id:0:1318898...'   Its presence provides comfort, but its official use is unknown thus far.


6. Live testing

After doing all the above, signing-up for C2DM, creating an APK, and installing it, I did the following:

Server-side registration request and response

I issued the following web request to C2DM using cURL and got the response:

~/sandbox$ curl -v -X POST -d "Email=<my_email>@gmail.com" -d "Passwd=<my_password>" -d "accountType=GOOGLE" -d "source=<my_package_name>" -d "service=ac2dm"  "https://www.google.com/accounts/ClientLogin"
* About to connect() to www.google.com port 443 (#0)
*   Trying 74.125.73.99... connected
* Connected to www.google.com (74.125.73.99) port 443 (#0)
>
> etc, etc, etc
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Cache-control: no-cache, no-store
< Pragma: no-cache
< Expires: Mon, 01-Jan-1990 00:00:00 GMT
< Date: Mon, 17 Oct 2011 00:28:18 GMT
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Content-Length: 818
< Server: GSE

SID=DQAAALUAAAAHcuF_ZOAiXnq1lFETmlU0pOZAF37oNlGkmXLGlRpS1MOPZlru1lPDvfBveeL...
LSID=DQAAALYAAACYYMPNPLuNwrH8QSRrzGWBmAbDzorzWnNvQBdYfVQZTAjWYDHQ7KIw8fLmPn...
Auth=DQAAALcAAACqNedh6Wg-zIupSaHqAAEWa2foDO9GtoRrNwd0KwVvWmRI9rXhXkCVuAEdiF...
* Connection #0 to host www.google.com left intact
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
~/sandbox$

I saved the Auth string as my 'server token' by manual copy-paste.

Android device registration

With my Android device connected to my computer via USB cable, I started logging debug messages:

    adb logcat

I started the application.

I found the following messages in the log from class HelloActivity.  These messages indicate that the activity started the registration request processing.

I/HelloActivity/onCreate(16293): Entry.
I/HelloActivity/registerWithC2DM(16293): Entry. Starting registration intent.
I/HelloActivity/registerWithC2DM(16293): Exit. Registration intent started.
I/HelloActivity/onCreate(16293): Exit.

Immediately next in the log, I found these messages from HelloRegistrationReceiver.  They indicate that registration was successful.  I saved the device registration ID string by manual copy-paste.

I/HelloRegistrationReceiver/onReceive(16293): Entry.
I/HelloRegistrationReceiver/onReceive(16293): action=com.google.android.c2dm.intent.REGISTRATION
I/HelloRegistrationReceiver/onReceive(16293): registrationId=gHcSt69NTZleRJ092QQRvegM7lKhkOUx3ngsFvX0G...
I/HelloRegistrationReceiver/onReceive(16293): error=null
I/HelloRegistrationReceiver/onReceive(16293): Exit.


Notification message generation

Back to the command-line, I issued the following web request to C2DM using cURL.  This request includes the server token string and the device registration ID string, manually inserted by copy-paste.

Note the message payload is "Eyeing pretty cURLs with bad intent".

~/sandbox$ curl  -v  -X POST  -H "Authorization: GoogleLogin auth=DQAAALcAAACqNedh6Wg-zIupSaHqAAEWa2foDO9GtoRrNwd0KwVvWmRI9rXhXkCVuAEdiF..."  -d "registration_id=gHcSt69NTZleRJ092QQRvegM7lKhkOUx3ngsFvX0G..."  -d "data.payload=Eyeing pretty cURLs with bad intent"  -d "collapse_key=0"  "https://android.apis.google.com/c2dm/send"
* About to connect() to android.apis.google.com port 443 (#0)
*   Trying 74.125.73.113... connected
* Connected to android.apis.google.com (74.125.73.113) port 443 (#0)
>
> etc, etc, etc...

< HTTP/1.1 200 OK
< Update-Client-Auth: DQAAALcAAABKauk6n7GUTKTvazwHtCBUuSiJU76WzEEwAK...
< Set-Cookie: DO_NOT_CACHE_RESPONSE=true;Expires=Mon, 17-Oct-2011 00:01:01 GMT
< Content-Type: text/plain
< Date: Mon, 17 Oct 2011 00:01:00 GMT
< Expires: Mon, 17 Oct 2011 00:01:00 GMT
< Cache-Control: private, max-age=0
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< Server: GSE
< Transfer-Encoding: chunked

id=0:1398806640216282%45ed4ba940600030
* Connection #0 to host android.apis.google.com left intact
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
~/sandbox$

Notification message receipt

Within a few seconds, my Android device received the notification message from C2DM.

I found the following messages in the log from class HelloMessageReceiver.  These indicate that the notification message was received.

I/HelloMessageReceiver/onReceive(16641): Entry.
I/HelloMessageReceiver/onReceive(16641): action=com.google.android.c2dm.intent.RECEIVE
I/HelloMessageReceiver/onReceive(16641): payload=Eyeing pretty cURLs with bad intent
I/HelloMessageReceiver/onReceive(16641): error=null
I/HelloMessageReceiver/onReceive(16641): Exit.

Observe that the expected message payload has been received "Eyeing pretty cURLs with bad intent".  This proves the notification system works.

Say 'woo hoo'.

Repeat

I repeated sending notification messages with a different message payload.   I received them succesfully on the Android app.


7. Production Notes

This article has presented an extremely simple low-function example.

Production apps should automatically propagate the device registration ID string from the Android app to the appserver.

Production apps must also add robustness.  For example, client-side Android apps must tolerate and retry or work around a variety of conditions, such as internet access disabled, going into or out of range, or a registration rejection from C2DM.  These may be handled in an Android Service.  Appserver-side apps must handle registration rejections during the initial attempt, as well as re-register gracefully whenever a notification message is rejected because the registration has expired.  And when the client propagates its device registration ID to the appserver, it must handle rejections and retry as well.


References:

All information for this work was learned from the following articles:

- Official doc:  http://code.google.com/android/c2dm/
- I started here:  http://blog.mediarain.com/2011/03/simple-google-android-c2dm-tutorial-push-notifications-for-android/
- More complex and realistic sample Android apps.  Click Source-> Browse-> trunk
  * http://code.google.com/p/jumpnote/
  * http://code.google.com/p/chrometophone/
- Android-side and server-side tutorial (my favorite):  http://www.vogella.de/articles/AndroidCloudToDeviceMessaging/article.html



Monday, August 29, 2011

Persistent local storage in a PhoneGap app using HTML5

I was trying to save a few bits of data across webkit browser invocations in a PhoneGap app.  My first approach was to save the data in a file, using PhoneGap's File API.  That got tedious fast.  Lucky for me, PhoneGap contributor Bryce suggested that I make use of a simple, new feature in HTML5: persistent local storage.

Google returned a good reference on the first search:  http://diveintohtml5.org/storage.html

I wrote two sandbox programs with three buttons to demonstrate how to save, fetch, and delete.  The first program handles a single string.

String demo


<!DOCTYPE HTML>
<html>
    <head>
        <title>HTML5 Persistent String</title>
        
        <script type="text/javascript" charset="utf-8">
        
            //---------------------------------------
            // Button handlers
            //---------------------------------------
            function setit() {
                var stringValue = "Hello World!";
                localStorage.setItem("string_key", stringValue);
                document.getElementById("stringResponseText").innerHTML = 
                    "Set string value: " + stringValue;
            }
            function nullit() {
                localStorage.setItem("string_key", null);
                document.getElementById("stringResponseText").innerHTML = 
                    "Set string value to null.";
            }
            function getit() {
                var stringValue = localStorage.getItem("string_key"); 
                document.getElementById("stringResponseText").innerHTML = 
                    "Got string value: " + stringValue; 
            }
        </script>
    </head>
    <body bgcolor="lightgreen">
        <h2>HTML5 Persistent String</h2>
        <input type="button" value="set" onclick="setit()" />
        <input type="button" value="get" onclick="getit()" />
        <input type="button" value="null" onclick="nullit()" />
        <p>
        <p id="stringResponseText"> </p>        
    </body>
</html>



Object demo

The second program follows the same pattern, but with a user-defined javascript object.  The secret here is to use JSON.stringify to convert your object into a 'flat' string for saving, and JSON.parse() to reconstitute the object after it is read.


<!DOCTYPE HTML>
<html>
    <head>
        <title>HTML5 Persistent Object</title>
        
        <script type="text/javascript" charset="utf-8">
        
            //---------------------------------------
            // Object definition and accessor
            //---------------------------------------
            function myObject(var1, var2, var3) {
                this.one = var1;
                this.two = var2;
                this.three = var3;
                this.toString = toString;
            }
            function toString() {
                return "one=" + this.one +
                       " two=" + this.two + 
                       " three=" + this.three;
            }
            
            //---------------------------------------
            // Button handlers.            
            //---------------------------------------
            function setIt() {
                var testObject = new myObject( "bird", "cat", "dog" );
                localStorage.setItem("object_key", JSON.stringify(testObject));
                document.getElementById("objectResponseText").innerHTML = 
                    "Set object value: " + testObject.toString();
            }
            function nullIt() {
                localStorage.setItem("object_key", null);
                document.getElementById("objectResponseText").innerHTML = 
                    "Set object value to null.";             
            }
            function getIt() {
                var stringifiedObject = localStorage.getItem("object_key");
                var reconstitutedObject = JSON.parse(stringifiedObject);
                if (reconstitutedObject) {
                    reconstitutedObject.toString = toString;
                    document.getElementById("objectResponseText").innerHTML = 
                        "Got object value: " + reconstitutedObject.toString(); 
                }
                else {
                    document.getElementById("objectResponseText").innerHTML = 
                        "Object value not found.";
                }
            }
        </script>
    </head>
    <body bgcolor="lightblue">
        <h1>HTML5 Persistent Object</h1>
        <h3>Object</h3>
        <input type="button" value="set" onclick="setIt()" />
        <input type="button" value="get" onclick="getIt()" />
        <input type="button" value="null" onclick="nullIt()" />
        <p>
        <p id="objectResponseText"> </p>        
    </body>
</html>




Testing

These sandbox programs can be browsed directly with a browser which supports HTML5, such as Chrome or Safari.

They can also be bundled into a PhoneGap app (via index.html), where they will work well installed on a mobile device.

Hope this is useful!

Friday, August 19, 2011

Including an optimized Dojo build within a PhoneGap app

In my last post, I showed how to create an optimized build of the Dojo Toolkit.  An optimized build contains the exact pieces of Dojo needed by your app, with nothing extra.  I served my app and the optimized Dojo from an Apache web server, and saw that it downloaded really fast.

Today I wondered if the same optimized build technique could be used in a hybrid mobile app created with the PhoneGap framework.  PhoneGap lets you build installable mobile apps where your app's content is written in HTML and JavaScript instead of each phone's proprietary language.  I had written hybrid apps which fetched Dojo from the Google CDN, and it worked fine.  But each time an app started, it had to download Dojo from the CDN.  That took time and bandwidth.  My goal was to include Dojo with my app, and see if it loaded instantly.

To my pleasant surprise, the optimized build worked exactly as expected.  Here is what I did...

First, I found an existing 'HelloPhoneGap' project for Android.  (You can create one from scratch using the instructions at Getting Started for your favorite mobile platform.)  The entry point for your app in a PhoneGap build environment is an index.html file within an assets/www directory.  For example:

.../HelloPhoneGap/assets/www/index.html

I modified the contents of index.html to point to the buildtest.html file created previously...


<!DOCTYPE HTML>
<html>
  <head>
    <title>PhoneGap</title>
  </head>
  <body bgcolor="yellow" >
  <h1>HelloPhoneGap</h1>
  <h2>from assets/www/index.html</h2>
  <ul>
      <li><a href="buildtest.html">optimized dojo build</a> 
  </ul>
  </body>
</html>



I copied the three files and directories created from the optimized build into the PhoneGap directory:

.../HelloPhoneGap/assets/www/buildtest.html
                             dojo/dojo.js
                             my/app.js

Then I compiled, installed, and started the app.  It worked out of the box.

When I started the app, the index.html file appeared.



I clicked the link to jump to the optimized dojo build page, buildtest.html.  The alert popped up:



After clicking 'OK', I got the version information.



Amazing.

Wednesday, August 17, 2011

How to create an optimized build for the Dojo Toolkit

Here is the step-by-step procedure I used to create an optimized dojo build.  This creates a dojo build which contain exactly what is needed by my app, no more, no less.    Using an optimized dojo build results in fewer downloads from the server, and less data transferred overall.

This procedure is based upon two reasonably good references:
It took me a while to make it all work because the filesystem locations were not obvious to me. 

Design Pattern

Adopt a design pattern where all your require() statements for dojo code are located in one javascript file.  This allows the dojo build process to create one new javascript file with the same name, to replace your original.  The new file replaces your list of dependents with the actual dojo javascript code.  

Prereqs

Fetch a dojo source build, for example:
Unzip this in a sandbox directory on your machine, for example:
    /home/sag/sandbox/dojo-release-1.6.1-src
where you will find subdirectories dojo, dijit, and dojox.

Coding

Here are two simple files I created, based upon first reference.  First the HTML:

/home/sag/sandbox/dojo-release-1.6.1-src/buildtest.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Tutorial: Hello Dojo!</title>

    <!-- load Dojo -->
    <script src="dojo/dojo.js"></script>

    <script>
        dojo.require("my.app");

        function init() {
            alert("Dojo ready, version:" + dojo.version);
            dojo.byId("greeting").innerHTML += 
                ". Greetings from dojo " + dojo.version;
        }
        dojo.ready(init);
    </script>
</head>
<body>
    <h1 id="greeting">Hello</h1>
</body>
</html>

and then the javascript:

/home/sag/sandbox/dojo-release-1.6.1-src/my/app.js

    dojo.provide("my.app");

    dojo.require("dojox.mobile.parser");
    dojo.require("dojox.mobile");
    dojo.require("dojox.mobile.compat");

You are now set up to do the build for this HTML/javascript.

Build

Go into the util/buildscripts directory.  For example:
    cd /home/sag/sandbox/dojo-release-1.6.1-src/util/buildscripts

Issue the build command.  Specify the path to your HTML file.  For example:
    ./build.sh 
      action=release 
      htmlFiles=/home/sag/sandbox/dojo-release-1.6.1-src/buildtest.html
It runs for a minute or so.

Inspect the results.  The important files are a new dojo.js and new app.js
    /home/sag/sandbox/dojo-release-1.6.1-src/release/dojo/dojo/dojo.js
                                                         /my/app.js

Test

Move the three files to the filesystem of your webserver.  For example, for apache:
    /var/www/html/dojo/dojo-opt/buildtest.html
                               /dojo/dojo.js
                               /my/app.js

Browse to the HTML and verify it works.

Verify Improvement

I captured screenshots of the downloads required when fetching the original HTML/javascript with the original dojo build.  It required more than a dozen file downloads.  (click the photo to enlarge)


Fetching the new HTML/javascript with optimized dojo results in three, much smaller downloads.