Thursday, February 16, 2012
Testing Android User Interfaces with Robotium
Overview
Android provides a 'test project' framework and various tools for testing apps. Robotium is an opensource project which enhances these facilities. Robotium provides convenience and utility methods which simplify clicking buttons, accessing text fields, etc. Robotium hides the underlying syntax and procedures.
Tests can be run on emulators as well as real devices.
Tests can be started within eclipse as well as from a command-line.
Testcase methods can access localized message strings.
Setup
- Create the Android app to be tested (aka app-under-test).
- Create a basic Android Test Project associated with your app-under-test. Follow basic Android instructions:
http://developer.android.com/resources/tutorials/testing/helloandroid_test.html
- Verify it runs.
From Eclipse package explorer, right-click the test project-> Run As-> Android JUnit Test.
- Fetch the Robotium jar file:
http://code.google.com/p/robotium/ -> Downloads -> robotium-solo-3.1.jar (or later)
Optional: Fetch the javadoc.
- Add the Robotium jar file to the test project 'libs' directory
From Eclipse-> right click test project-> Properties-> Java build path-> Add external jars-> point to jar.
- In your testcase class, import the Robotium Solo class and define a reference to a Solo object:
import com.jayway.android.robotium.solo.Solo;
private Solo mSolo;
Fix your dependencies and get your testcase class to compile.
- Instantiate a Solo object in one of your testcase methods:
// Instantiate a new Solo object.
mSolo = new Solo(getInstrumentation(), getActivity());
Verify it runs. You are now ready to make use of Robotium...
Using Robotium
Study the methods available in class Solo. Either read the javadoc or browse the source:
https://github.com/jayway/robotium/tree/master/robotium-solo/src/main/java/com/jayway/android/robotium/solo
- Access widgets in your app-under-test using Solo. Examples:
-- My app-under-test contains one ToggleButton. Since there is only one, I can use the index '0' to query this ToggleButton:
// Query whether the button is checked.
boolean checked = mSolo.isToggleButtonChecked(0)
-- My ToggleButton text is defined as 'OFF' and 'ON'. To click the ToggleButton, I use the name:
// Click the OFF button to ON.
mSolo.clickOnToggleButton("OFF");
Note that the 'name' of a ToggleButton changes from 'OFF' to 'ON' depending upon its state. To click it again when it is on, issue the following:
// Click the ON button to OFF.
mSolo.clickOnToggleButton("ON");
-- My app also has one TextView which displays various messages. I can use Robotium Solo to query if a message is visible:
// Query whether the message is displayed.
boolean found = mSolo.searchText("green apples");
-- Robotium also has a set of methods named 'waitForText' with various parameters. I look forward to using these.
Internationalization
Rich apps display text messages in various languages. Testcases which search for text strings must be able to access the localized string. The basic Android test project supports this, and it works with Robotium as well.
I added several strings to be translated in my app-under-test. From res/values/strings.xml:
<string name="on">ON</string>
<string name="off">OFF</string>
<string name="green_apples">green apples</string>
I internationalized my toggle button to use these values:
<ToggleButton
...
android:textOn="@string/on"
android:textOff="@string/off" >
</ToggleButton>
Then within the test project...
// Get localized strings.
int idOff = com.example.app-under-test-package-name.R.string.off;
String off = getActivity().getResources().getString(idOff);
// Click the OFF button to ON.
mSolo.clickOnToggleButton(off);
Similarly for text fields...
int idGreenApples = com.example.app-under-test-package-name.R.string.green_apples;
String greenApples = getActivity().getResources().getString(idGreenApples);
// Query whether the message is displayed.
boolean found = mSolo.searchText(greenApples);
Execution
There are two ways to invoke tests...
From eclipse package explorer, right-click the test project-> Run As-> Android JUnit Test. Eclipse first installs the app-under-test APK and the testcase APK. Then it switches to the familiar JUnit view which shows a list of tests as they run, along with a progress bar.
From a command-line, issue ADB commands to install both APKs, then issue an ADB command to start the testcase app (specifying your testcase package name and the Android InstrumentationTestRunner). Example:
# Install the app-under-test.
adb install -r app-under-test.apk
# Install the test app.
adb install -r testcases.apk
# Start the test app:
adb shell am instrument -w com.example.test-package-name/android.test.InstrumentationTestRunner
In both cases, you can watch the tests run on your emulator or real device.
Execution on multiple devices
If you have several real devices attached to your computer via USB cable, you can run all the tests in parallel, from different command-line windows (or through fancy scripting). To do this, you must specify the device ID. First query the list of devices (obfuscated results):
adb devices
List of devices attached
S84KD9G0D0F4 device
P40D9GKKRKD8 device
Then target each ADB command to a specific device. Example:
adb -s S84KD9G0D0F4 install -r app-under-test.apk
Summary
Robotium provides a large set of convenience methods for accessing widgets and text fields, which makes your testcases smaller, simpler, and easier to understand. Very nice. Study the methods and make use of them.
References:
Android Test Guide: http://developer.android.com/guide/topics/testing/testing_android.html
Robotium: http://robotium.org
Tutorials: http://lmgtfy.com/?q=robotium+tutorial
Monday, February 6, 2012
How to use variables in Android internationalized message strings
This article explains how to include variables within internationalized message strings for Android applications. It also explains how to specify an explicit locale (ie, language) for messages.
All secrets were gleaned from the references listed at bottom.
Prereqs
This article assumes you have:
- Set up the Android SDK and can build, install, and run an APK.
- Created a message file .../res/values/strings.xml
- Can access messages in the file using Context.getResources()...
Default locale messaging, no variables
In basic messaging, the simple Android method getString() may be used to get a message in the default locale.
In res/values/strings.xml:
<string name="hello_message">Hello World</string>
In the java class:
String message = myContext.getResources().getString(R.string.hello_message);
The resulting string 'message' is returned in the default locale:
Hello World
Default locale messaging, 1 variable
To include a variable inside the message, Java formatting strings such as %d (int) and %s (string) may be included in strings.xml. (See a full list of formatting strings in the references below.)
In res/values/strings.xml:
<string name="ate_n_donuts">I ate %d donuts.</string>
In the java class:
int numberDonuts = 12;
String message = myContext.getResources().getString(R.string.ate_n_donuts, numberDonuts);
The resulting string 'message' is returned in the default locale:
I ate 12 donuts.
Default locale messaging, multiple variables
Multiple variables in a string must be numbered with 1$, 2$, 3$. These sequence numbers are embedded inside the formatting string, %s or %d, to appear as: %1$s, %2$s, etc. (very odd.)
Take note the sequence starts at 1, not zero.
The numbers correspond to the sequence of variables in the java method parameters. This scheme allows translators to modify the sequence of variables to match the natural language style. Example:
In res/values/strings.xml:
<string name="game_results">The %1$s beat the %2$s in the %3$s.</string>
In your java class:
String team1 = "New York Giants"
String team2 = "New England Patriots"
String gameName = "Super Bowl";
String message = myContext.getResources().getString(R.string.game_results, team1, team2, gameName);
The resulting string 'message' is returned in the default locale:
The New York Giants beat the New England Patriots in the Super Bowl.
Explicit locale messaging
The previous examples returned strings in the default locale of the mobile device automatically. Messages displayed to the user should be presented this way. However, for debug and servicability purposes, it may be useful to present messages in the language of the developer. For example, I always want my log files in English.
To present a message in an explicit locale, the native Java formatter must be used. In this last example, notice the variable is passed-in to the Java String.format() method, instead of to the Android getString() method. Repeating the donut example...
In res/values/strings.xml:
<string name="ate_n_donuts">I ate %d donuts.</string>
In the java class:
int numberDonuts = 12;
Locale logLocale = Locale.US;
String messageEnglish = String.format(logLocale,
myContext.getResources().getString(R.string.ate_n_donuts),
numberDonuts);
The resulting string 'message' is returned in the explicitly-specified locale:
I ate 12 donuts.
Really complex messaging
You now have all the secrets. You can specify multiple variables in an internationalized message string. You can get strings in the default locale and a specific locale.
And you can see that your code will get really complex and ugly fast.
Recommendations
I recommend you study the fine points in these examples. Then write some helper methods to hide all these details from the main flow of your application.
Good luck!
Excellent References:
Bram's blog with comment from Romain Guy:
http://bvirtual.nl/2009/11/variable-in-androids-stringsxml.html
Official Android docs:
http://developer.android.com/reference/java/lang/String.html#format(java.lang.String, java.lang.Object...)
http://developer.android.com/reference/java/util/Formatter.html
http://developer.android.com/reference/android/content/Context.html#getString%28int,%20java.lang.Object...%29
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.
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!
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!
Subscribe to:
Posts (Atom)





