Jun 06

Urban Airship 1.0 integration with an Android Phonegap app

Since I wrote my previous post, UrbanAirship has released the final version of their android library. Oh well... the new version comes with some new shiny features like analytics for your app, the means to customize the look and feel of notifications (including sounds) and the possibility of using C2DM as your transport medium instead of helium. (It seems this feature was already present in the version I was using before but the docs back didn't mention it). Here you can find an updated howto of how to integrate it into your phonegap project:

AndroidManifest.xml

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

    <supports-screens
        android:largeScreens="true"
        android:normalScreens="true"
        android:smallScreens="true"
        android:resizeable="true"
        android:anyDensity="true"
    />

    <application android:debuggable="true" android:icon="@drawable/icon" android:label="@string/app_name" android:name=".MainApplication">
        <activity android:configChanges="orientation|keyboardHidden" android:name="com.example.MainActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- REQUIRED -->
        <receiver android:name="com.urbanairship.CoreReceiver">
            <!-- REQUIRED IntentFilter - For Helium and Hybrid -->
            <intent-filter>
              <action android:name="android.intent.action.BOOT_COMPLETED" />
              <action android:name="android.intent.action.ACTION_SHUTDOWN" />
            </intent-filter>
        </receiver>

        <!-- REQUIRED for C2DM and Hybrid -->
        <receiver android:name="com.urbanairship.push.c2dm.C2DMPushReceiver"
                android:permission="com.google.android.c2dm.permission.SEND">
          <!-- Receive the actual message -->
          <intent-filter>
              <action android:name="com.google.android.c2dm.intent.RECEIVE" />
              <category android:name="com.example" />
          </intent-filter>
          <!-- Receive the registration id -->
          <intent-filter>
              <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
              <category android:name="com.example" />
          </intent-filter>
        </receiver>

        <!-- REQUIRED -->
        <!-- The 'android:process' parameter is optional. Set it to a value starting
            with a colon (:) to make it run in a separate, private process -->
        <service android:name="com.urbanairship.push.PushService"
                android:process=":com.urbanairship.push.process"/>

        <!-- OPTIONAL, if you want to receive push, push opened and registration completed intents -->
        <receiver android:name="com.example.IntentReceiver" />
    </application>
    <!-- REQUIRED -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <!-- REQUIRED for C2DM  -->
    <!-- Only this application can receive the messages and registration result -->
    <permission android:name="com.example.permission.C2D_MESSAGE" android:protectionLevel="signature" />
    <uses-permission android:name="com.example.permission.C2D_MESSAGE" />

    <!-- This app has permission to register and receive message -->
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
</manifest>

PushNotification.js

function PushNotification() {
}

PushNotification.prototype.registerCallback = function(successCallback, failureCallback) {
     return PhoneGap.exec(
            successCallback,           // called when signature capture is successful
            failureCallback,           // called when signature capture encounters an error
            'PushNotificationPlugin',  // Tell PhoneGap that we want to run "PushNotificationPlugin"
            'registerCallback',        // Tell the plugin the action we want to perform
            []);                       // List of arguments to the plugin
};

PushNotification.prototype.notificationCallback = function (json) {
    var data = Ext.util.JSON.decode(json);
    Ext.Msg.alert("Success", data.msg);
};

PhoneGap.addConstructor(function() {
    if (typeof navigator.pushNotification == "undefined")
        navigator.pushNotification = new PushNotification();
});

PushNotificationPlugin.java

package com.example;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.util.Log;

import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import com.phonegap.api.PluginResult.Status;

public class PushNotificationPlugin extends Plugin {
    final static String TAG = PushNotificationPlugin.class.getSimpleName();

    static PushNotificationPlugin instance = null;

    public static final String ACTION = "registerCallback";

    public static PushNotificationPlugin getInstance() {
        return instance;
    }

    public void sendResultBack(String msg, String payload) {
        JSONObject data = new JSONObject();
        try {
            data.put("msg", msg);
            data.put("payload", payload);
        } catch (JSONException e) {
            Log.e(TAG, e.getMessage());
        }
        String js = String.format("navigator.pushNotification.notificationCallback('%s');", data.toString());
        //Log.d(TAG, "Sending javascript " + js);
        this.sendJavascript(js);
    }

    @Override
    public PluginResult execute(String action, JSONArray data,
            String callbackId) {

        instance = this;

        PluginResult result = null;
        if (ACTION.equals(action)) {
            result = new PluginResult(Status.NO_RESULT);
            result.setKeepCallback(false);
        } else {
            Log.d(TAG, "Invalid action: " + action + " passed");
            result = new PluginResult(Status.INVALID_ACTION);
        }
        return result;
    }
}

MainApplication.java

package com.example;

import android.app.Application;

import com.urbanairship.UAirship;
import com.urbanairship.push.PushManager;

public class MainApplication extends Application {

    final static String TAG = MainApplication.class.getSimpleName();

    @Override
    public void onCreate() {
        super.onCreate();

        UAirship.takeOff(this);
        PushManager.enablePush();
        PushManager.shared().setIntentReceiver(IntentReceiver.class);
    }

    public void onStop() {
        UAirship.land();
    }
}

MainActivity.java

package com.example;

import android.os.Bundle;

import com.phonegap.DroidGap;
import com.urbanairship.UAirship;

public class MainActivity extends DroidGap {

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.loadUrl("file:///android_asset/www/index.html");
        // phonegap plugins
        super.addService("PushNotificationPlugin", "com.example.PushNotificationPlugin");
    }

    @Override
    public void onStart() {
        super.onStart();
        UAirship.shared().getAnalytics().activityStarted(this);
    }

    @Override
    public void onStop() {
        super.onStop();
        UAirship.shared().getAnalytics().activityStopped(this);
    }
}

IntentReceiver.java

package com.example;

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

import com.urbanairship.UAirship;
import com.urbanairship.push.PushManager;

public class IntentReceiver extends BroadcastReceiver {

    private static final String TAG = IntentReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG, "Received intent: " + intent.toString());
        String action = intent.getAction();

        if (action.equals(PushManager.ACTION_PUSH_RECEIVED)) {
            int id = intent.getIntExtra(PushManager.EXTRA_NOTIFICATION_ID, 0);

            Log.i(TAG, "Received push notification. Alert: " + intent.getStringExtra(PushManager.EXTRA_ALERT)
                + ". Payload: " + intent.getStringExtra(PushManager.EXTRA_STRING_EXTRA) + ". NotificationID="+id);

            String alert = intent.getStringExtra(PushManager.EXTRA_ALERT);
            String extra = intent.getStringExtra(PushManager.EXTRA_STRING_EXTRA);

            PushNotificationPlugin plugin = PushNotificationPlugin.getInstance();
            plugin.sendResultBack(alert, extra);

        } else if (action.equals(PushManager.ACTION_NOTIFICATION_OPENED)) {
            Log.i(TAG, "User clicked notification. Message: " + intent.getStringExtra(PushManager.EXTRA_ALERT)
                    + ". Payload: " + intent.getStringExtra(PushManager.EXTRA_STRING_EXTRA));

            Intent launch = new Intent(Intent.ACTION_MAIN);
            launch.setClass(UAirship.shared().getApplicationContext(), MainActivity.class);
            launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            UAirship.shared().getApplicationContext().startActivity(launch);

        } else if (action.equals(PushManager.ACTION_REGISTRATION_FINISHED)) {
            Log.i(TAG, "Registration complete. APID:" + intent.getStringExtra(PushManager.EXTRA_APID)
                    + ". Valid: " + intent.getBooleanExtra(PushManager.EXTRA_REGISTRATION_VALID, false));
        }

    }
}

Follow the rest of the instructions regarding airshipconfig.properties and you will be set. The folks from UrbanAirship suggested me that I should mention that most of the people will want to use the c2dm transport instead of helium. Check out their plans and decide by yourself what are your requisites and whether the basic plan fits your needs or go premium instead.

This is an example airshipconfig.properties:

developmentAppKey = yourDevelopmentAppKey
developmentAppSecret = yourDevelopmentAppSecret
productionAppKey = yourProductionAppKey
productionAppSecret = yourProductionAppSecret

#transport is "helium", "c2dm" or "hybrid"
transport = c2dm

c2dmSender = authorized-c2dmSender@gmail.com
inProduction = false

You just need now to generate an auth token executing python ua-android-lib-latest/tools/clientauth.py and paste the result in the "C2DM Authorization Token" of your urbainairship app (webpage).

I've uploaded to my github account a sample project that you can use as a base for your app. Please have a look and let me know what you think.

UPDATE: Added UAirship.land() in onStop()

UPDATE2: Mention that c2dm should be used instead of helium, show how to configure a C2DM based app. Add a sample project that summarizes this post.