Android: Advanced Feature Setup

Actions

Urban Airship Actions provides a convenient way to automatically perform tasks by name in response to push notification, Rich App Page or Landing Page interactions and JavaScript.

An action is an abstraction over a unary function, which takes an argument and performs a defined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context.

The Urban Airship library comes with pre-made actions for common tasks such as setting/modifying tags, showing a landing page, enabling deep linking and opening URLs out of the box. Actions can also be extended to enable custom application behaviors and engagement experiences.

Actions are sent with a push by supplying a JSON payload as a push extra with the key com.urbanairship.actions. The JSON payload is a map of the action names to action values. The values can be any valid JSON value.

When the push is received, actions will be triggered with the situation Situation.PUSH_RECEIVED and again when the push is opened with the situation Situation.PUSH_OPENED. Any actions triggered through the Javascript bridge, or through intercepted URL in a webview will be triggered with Situation.WEB_VIEW_INVOCATION. Any other way of triggering actions should be invoked with situation Situation.MANUAL_INVOCATION. The different situations allows actions to determine if they should run or not, and possibly do different behavior depending on the situation.

Set up

Actions only require AndroidManifest.xml modifications in order to be fully functional out of the box.

Under the application tag, add:

<activity android:name="com.urbanairship.actions.ActionActivity"/>

<!-- MODIFICATION REQUIRED - Replace parentActivityName to an activity in the project -->
<activity
    android:name="com.urbanairship.actions.LandingPageActivity"
    android:parentActivityName=".MainActivity"
    android:exported="false">

   <!-- MODIFICATION REQUIRED - Replace parentActivityName to an activity in the project -->
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value="com.urbanairship.push.sample.MainActivity" />

    <intent-filter>
        <action android:name="com.urbanairship.actions.SHOW_LANDING_PAGE_INTENT_ACTION"/>
        <data android:scheme="http" />
        <data android:scheme="https" />
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

<service android:name="com.urbanairship.actions.ActionService" />

Some actions might launch activities as part of the perform step, so if the application also launches an activity when a push is opened in the IntentReceiver it may cause a race condition of what activity is shown. To prevent this issue, modify the IntentReceiver to look for actions in the Intent before defaulting to launching an activity.

public class IntentReceiver extends BroadcastReceiver {

  // A set of actions that launch activities when a push is opened.  Update
  // with any custom actions that also start activities when a push is opened.
  private static String[] ACTIVITY_ACTIONS = new String[] {
     DeepLinkAction.DEFAULT_REGISTRY_NAME,
     OpenExternalUrlAction.DEFAULT_REGISTRY_NAME,
     LandingPageAction.DEFAULT_REGISTRY_NAME
  };

  @Override
  public void onReceive(Context context, Intent intent) {

    if (PushManager.ACTION_PUSH_RECEIVED.equals(intent.getAction())) {
      // Push received
    } else if (PushManager.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
      // Push opened

      // Only launch the main activity if the payload does not contain any
      // actions that might have already opened an activity
      if (!ActionUtils.containsRegisteredActions(intent.getExtras(), ACTIVITY_ACTIONS)) {
        Intent launch = new Intent(Intent.ACTION_MAIN);
        launch.setClass(context, MainActivity.class);
        launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(launch);
      }
    }
  }
}

Action Registry

The action registry is the central place to register actions by name. Each entry in the registry contains an action, the names that the action is registered under, a predicate that allows filtering when an action can run, and allows specifying alternative actions for different situations.

Registering an action:

Action customAction = new CustomAction();
ActionRegistry.Entry entry = ActionRegistery.shared().registerActiion(customAction, "my_action_name");

Looking up an entry:

ActionRegistry.Entry entry = ActionRegistery.shared().getEntry("my_action_name");

Setting a predicate:

// Predicate that will reject PUSH_RECEIVED causing the action to never run during that situation.
Predicate<ActionArguments> rejectPushReceivedPredicate = new Predicate<ActionArguments>() {
    @Override
    public boolean apply(ActionArguments arguments) {
        return !(Situation.PUSH_RECEIVED.equals(arguments.getSituation()));
    }
};

entry.setPredicate(rejectPushReceivedPredicate);

Triggering Actions

To guarantee proper execution of actions, all actions should run through the action runner. The action runner is a convenient way to execute an action synchronously, or asynchronously with an optional callback. The runner is able to run an action directly, or by specifying the name of an action in the registry.

Example:

ActionArguments args = new ActionArguments(Situation.MANUAL_INVOCATION, "my argument");

// Running an action directly through the runner
ActionRunner.shared().runAction(customAction, args);

// Running an action by registered name
ActionRunner.shared().runAction("my_action_name", args);

// An optional callback when finished
ActionRunner.shared().runAction("my_action_name", args, new ActionCompletionCallback() {
    public void onFinish(ActionResult result) {
        Logger.info("Action finished!  Result: " + result);
    }
});

// Block until the action finishes
ActionResult result = ActionRunner.shared().runActionSync("my_action_name", args);

Landing Page Action

The landing page action allows showing a rich content page when a user clicks a notification. By default, it will be shown as a full screen activity using the application theme. An action bar will be shown if the landing page is shown on a device running HONEYCOMB or higher (3.0). A guide to fully customize the landing page can be found here: Android Landing Page Customization.

Accepted Arguments
  • URL defined as a String, URL, or Uri

  • A Map containing:
    • url: The defined URL
    • cache_on_receive: flag to enable or disable caching when a PUSH_RECEIVED. Defaults to false.
Default Action Names
  • landing_page_action
  • ^p
Accepted Situations
  • Situation.PUSH_OPENED
  • Situation.PUSH_RECEIVED
  • Situation.WEB_VIEW_INVOCATION
  • Situation.MANUAL_INVOCATION

When the landing page is invoked with PUSH_RECEIVED, it will only cache the landing page content in the background instead of displaying it.

Default Registry Predicate

Rejects Situation.PUSH_RECEIVED if the application has not been opened in the last week.

Example:

ActionArguments args = new ActionArguments(Situation.MANUAL_INVOCATION, "http://wwww.urbanairship.com");
ActionRunner.shared().runAction("landing_page_action", args);

Open External URL Action

The open external URL action allows launching any URL. The action will construct an intent with a given Uri and try to start an activity with it for viewing.

Accepted Arguments
  • String
  • URL
  • Uri
Default Action Names
  • open_external_url_action
  • ^u
Accepted Situations
  • Situation.PUSH_OPENED,
  • Situation.WEB_VIEW_INVOCATION
  • Situation.MANUAL_INVOCATION

Example:

ActionArguments args = new ActionArguments(Situation.MANUAL_INVOCATION, "http://www.urbanairship.com");
ActionRunner.shared().runAction("open_external_url_action", args);

Add Tags Action

The add tags action allows adding one or more tags to the device.

Accepted Arguments
  • String for a single tag
  • JSON array for multiple tags
Default Action Names
  • add_tags_action
  • ^+t
Accepted Situations
  • Situation.PUSH_OPENED,
  • Situation.WEB_VIEW_INVOCATION
  • Situation.MANUAL_INVOCATION
  • Situation.PUSH_RECEIVED

Default Registry Predicate

Rejects Situation.PUSH_RECEIVED.

Example:

ActionArguments args = new ActionArguments(Situation.MANUAL_INVOCATION, "tagOne");
ActionRunner.shared().runAction("add_tags_action", args);

Remove Tags Action

The remove tags action allows removing one or more tags from the device.

Accepted Arguments
  • String for a single tag
  • JSON array for multiple tags
Default Action Names
  • remove_tags_action
  • ^-t
Accepted Situations
  • Situation.PUSH_OPENED,
  • Situation.WEB_VIEW_INVOCATION
  • Situation.MANUAL_INVOCATION
  • Situation.PUSH_RECEIVED

Default Registry Predicate

Rejects Situation.PUSH_RECEIVED.

Example:

ActionArguments args = new ActionArguments(Situation.MANUAL_INVOCATION, "tagOne");
ActionRunner.shared().runAction("remove_tags_action", args);

Custom Actions

The action framework supports any custom actions. Create an action by extending the base action class, then register the action to the registry after takeoff. The action can then be triggered the same way as the built-in actions.

Note

Actions that are either long lived or are unable to be interrupted by the device going to sleep should request a wake lock before performing. This is especially important for actions that are performing in Situation.PUSH_RECEIVED, when a push is delivered when the device is not active.

Example:

public class CustomAction extends Action {

    @Override
    public boolean acceptsArguments(ActionArguments arguments) {
        if (!super.acceptsArguments(arguments)) {
            return false;
        }

        // Do any argument inspections.  The action will stop
        // execution if this method returns false.

        return true;
    }

    @Override
    public void onStart(String actionName, ActionArguments arguments) {
        Logger.info("Action started!");

        // Called before the perform
    }

    @Override
    public ActionResult perform(String actionName, ActionArguments arguments) {
        Logger.info("Action is performing!");

        return ActionResult.newEmptyResult();
    }

    @Override
    public void onFinish(String actionName, ActionArguments arguments, ActionResult result) {
        Logger.info("Action finished!");

        // Called after the action performed
    }
}

Register the action after takeoff:

UAirship.takeoff();

ActionRegister.shared().registerAction(new CustomAction(), "my_custom_action");

Rich Push

Sending Rich Push Messages through the REST API

Note

As of the release of the Urban Airship Push API v3, the operation of sending a rich message to an inbox within the application is no longer handled via the Rich Push endpoints at /api/airmail/.

Rich content which is optionally associated with a push notification is now included in the message component of the Push API at /api/push/. See the Rich App Pages documentation for more detail.

Apart from the android dictionary required to send an Android push backing the rich message, this is done the same way as you would find on iOS. Sending a JSON payload with this structure as a body to the following URL:

https://go.urbanairship.com/api/push/

will deliver the message. For more information on the REST API, see our Rich Push server side documentation.

Library Components

Below are a few basic definitions of the classes used for interacting with the Rich Push API.

For more detail, please see the Urban Airship Android Library Reference

For more information on getting started with Rich Push on Android, see the Android Rich Push Tutorial.

RichPushManager

The rich push manager is the primary interface for rich push. It gives you access to the RichPushUser and provides ways to refresh messages.

RichPushUser

The rich push user is the basic building block of Rich Push and one is automatically created for you when your application is installed on a device. The rich push user has access to the ID, user token, and the rich push inbox.

RichPushInbox

The rich push inbox is the access point for the user’s rich push messages. The inbox provides methods to delete messages, mark messages as read and unread, and is automatically updated with Urban Airship.

RichPushMessage

Each rich push message contains information about a user’s Rich Application page such as sent date, title, read/unread status, extras, and the url of the message’s content.

RichPushMessageWebView

An extended WebView that is configured to display the content of a RichPushMessage. Only available in API 5 (Eclair) and higher.

UAWebViewClient

A web view client that allows running Urban Airship Actions through Javascript and handles any user authorization for Rich Application Pages. Extend this class when setting a custom web view client for a RichPushMessageWebView.

UAJavascriptInterface

The Urban Airship Javascript interface. Provides Javascript calls to trigger actions and to retrieve information about the message such as the title and send date.

Message Operations

Note

Please note that in order to reduce network traffic and help battery life, modifications to RichPushMessages aren’t immediately synced with the server. The RichPushUpdateService will make sure your changes are synced to the server in a timely fashion or you can manually sync which you’ll learn about in the next section.

Marking messages read

A RichPushMessage really only has one mutable state, whether it’s read or not. As of this writing, there’s no API endpoint for marking a message unread, but we do support marking it unread locally if you want to allow your users to do so. You can change message read state one of two ways.

Individually:

// mark it read
message.markRead();

// mark it unread
message.markUnread();

or in bulk:

// mark messages read
RichPushManager.shared().getRichPushUser().getRichPushInbox().markMessagesRead(someSetOfIds);

// mark messages unread
RichPushManager.shared().getRichPushUser().getRichPushInbox().markMessagesUnread(someSetOfIds);

Deleting a message

Just like updating the read status of a RichPushMessage, we can delete in one of two ways.

Individually:

// delete it
message.delete();

or in bulk:

// delete a bunch of messages using the inbox and a set of message ids
RichPushManager.shared().getRichPushUser().getRichPushInbox().deleteMessages(someSetOfIds);

Note

Be sure you want to do this, as the message is unrecoverable.

Listening for updates to RichPushInbox and RichPushUser

In certain circumstances, you’re going to want to listen for updates to the RichPushInbox and maybe even the RichPushUser (although this is more rare). In order to do this, you’ll need to implement the RichPushManager.Listener interface:

public class RichPushListener implements RichPushManager.Listener {

        @Override
        public void onUpdateMessages(boolean success) {
                messagesRefreshingAnimation.stop();
        }

        @Override
        public void onUpdateUser(boolean success) {
                userUpdatingAnimation.stop();
        }
}

Once you have your class created, you need to tell the RichPushManager where to find it.

For example, in an Activity:

// onCreate
RichPushListener richPushListener = new RichPushListener();
// onResume
RichPushManager.shared().addListener(richPushListener);

and in order to avoid leaking your listener:

// onPause
RichPushManager.shared().removeListener(richPushListener);

Finally, request a refresh of your RichPushInbox or an update of your RichPushUser:

RichPushManager.shared().refreshMessages();
RichPushManager.shared().updateUser();

The RichPushInbox.Listener has a single onUpdateInbox method that notifies whenever the inbox changes. This is used in the RichPushSample to update both the InboxFragment as well as the MessageFragmentAdapter from the InboxActivity:

@Override
public void onUpdateInbox() {
    updateRichPushMessages();
}

/**
 * Grabs the latest messages from the rich push inbox, and syncs them
 * with the inbox fragment and message view pager if available
 */
private void updateRichPushMessages() {
    messages = RichPushManager.shared().getRichPushUser().getInbox().getMessages();
    this.inbox.setMessages(messages);
    if (messagePager != null) {
        ((MessageFragmentAdapter) messagePager.getAdapter()).setRichPushMessages(messages);
    }
}

Tags & Aliases

To help target specific devices or users for a notification, we have Aliases and Tags.

Tags

Tags are a feature that allow you to attribute any arbitrary metadata to a specific device. Common examples include favorites such as sports teams or news story types.

Example:

Set<String> tags = new HashSet<String>();
tags.add("Some-Tag");
PushManager.shared().setTags(tags);

Aliases

Aliases allow you to associate multiple device with one particular user or alias.

Example:

PushManager.shared().setAlias("AliasString");

Analytics Reporting

This document is a quick guide to integrating Reports support with your Android application using the Urban Airship client library. We’ll start with a discussion of the minimum code necessary to take advantage of these features, and briefly cover the event upload model the client library implements.

Setting up Analytics: Minor Assembly Required

Our Android client library ships with analytics support nearly ready to go. However, due to the way Android Activities work, there are a few changes you will need to make to your Activities in order for the library to be able to determine when your app passes into the foreground or background, which are crucial to reporting all metrics.

Instrumenting Your Activities

Note

Starting with Android API 14, the client library can now detect when an activity is started or stopped without any manual instrumentation. If your app’s minSDK >= 14 (Ice Cream Sandwich), you no longer need to modify any of your activities. Make sure to set the minSDK in the airshipconfig.properties in order to prevent any missing instrumented analytic warnings.

Android has no explicit events an app developer can listen for that indicate whether an app is running in the foreground or the background, since by default, apps continue running regardless of which Activity is currently displayed. The fluid transitions that are possible between the Activities of different apps further complicate this conceptual model. However, for our purposes it is safe to assume that if none of your app’s Activities are currently being shown, the app is “in the background”, and should not count as time that the user spends interacting with it.

The client library measures this by keeping track of when your application’s Activities start and stop, which correspond with their display. Because the library cannot detect this directly on older Android APIs, there are two ways to facilitate this, both of which require a small amount of manual intervention.

The easiest way to do this is to subclass InstrumentedActivity and InstrumentedListActivity, both of which are provided with the library. Using subclasses of these Activities ensures that whenever your Activity is presented or hidden from the user, that this information is passed along to the library.

If you would like to manually instrument your class, simply update your Activity’s onStart and onStop methods with the following:

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

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

Analytics Events and Uploading

Our client library stores events in a local database and uploads them periodically in a background thread. We’ve taken great care to make sure that the database won’t grow beyond a small fixed size, so extended periods of lost connectivity are nothing to worry about. The event upload thread is woken up when new events are triggered and goes to sleep when there are no more events to process, so the impact on battery life is negligible.

Disabling Analytics

If you do not wish to include analytics and reporting in your application, simply add the following line to your airshipconfig.properties file:

analyticsEnabled = false

Preferences

Push Preferences (Enable, Disable, Sound, Vibrate)

Push is disabled by default. To enable push notifications in an app, call PushManager.enablePush(). Push can be disabled by calling PushManager.disablePush().

Enabling and Disabling Push

// Handling user push preferences
if (userRequestedPush) {
    PushManager.enablePush();
} else {
    PushManager.disablePush();
}

We do not recommend triggering these calls directly from a checkbox or other UI widget. These calls immediately start the registration process, so it is best to call from a location where they can’t be toggled on and off quickly.

Other options are also available via PushPreferences.

PushPreferences prefs = PushManager.shared().getPreferences();
prefs.setSoundEnabled(boolean); // enable/disable sound when a push is received
prefs.setVibrateEnabled(boolean); // enable/disable vibrate on receive

Preferences Screen

You can easily add Urban Airship preferences to the android preference screens. The library provides several ready to use preferences that can be mixed in with non UA preferences to allow the user to enable/disable push, sound, vibration, location, set quiet times, set aliases, and add tags.

To add Urban Airship preferences, first define an xml preference file:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <com.urbanairship.preference.PushEnablePreference
        android:key="push_preference"
        android:persistent="false"
        android:title="@string/push_preference_title" />

    <com.urbanairship.preference.SoundEnablePreference
        android:dependency="push_preference"
        android:key="sound_preference"
        android:persistent="false"
        android:title="@string/sound_preference_title" />

</PreferenceScreen>
Modify your PushPreferencesActivity or PreferenceFragment:
  • onCreate : Create a UAPreferenceAdapter with the preference screen
  • onStop: Call the UAPreferenceAdapter’s applyUrbanAirshipPreferences() to save the preferences
public class PushPreferencesActivity extends SherlockPreferenceActivity {

    private UAPreferenceAdapter preferenceAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Add the preferences defined in xml above
        this.addPreferencesFromResource(R.xml.push_preferences);

        // Creates the UAPreferenceAdapter with the entire preference screen
        preferenceAdapter = new UAPreferenceAdapter(getPreferenceScreen());
    }

    @Override
    protected void onStop() {
        super.onStop();

        // Apply any changed UA preferences from the preference screen
        preferenceAdapter.applyUrbanAirshipPreferences();
    }
}

If you want to customize any of the preferences views or behavior, just extend the preference or create a new preference that implements the UAPreference interface.

For example, to create a new PushEnablePreference:

public class PushEnablePreference extends CheckBoxPreference implements UAPreference  {

    public PushEnablePreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public PreferenceType getPreferenceType() {
        return PreferenceType.PUSH_ENABLE;
    }

    @Override
    public void setValue(Object value) {
        this.setChecked((Boolean) value);
    }
}

The setValue(Object value) will be called on creating the UAPreferenceAdapter. The preferences will then be saved once the preference adapter’s applyUrbanAirshipPreferences() method is called.

Customizing Push Notifications

PushNotificationBuilder Interface

All incoming push notifications are built using a class that implements the PushNotificationBuilder interface. You can skip using the BasicPushNotificationBuilder and CustomPushNotificationBuilder and provide a notification builder that implements this interface.

Example:

public MyNotificationBuilder implements PushNotificationBuilder {
    //Implementing PushNotificationBuilder
    @Override
    public Notification buildNotification(String alert, Map<String, String> extras) {
        Context ctx = UAirship.shared().getApplicationContext();

        // Create a notification
        Notification notification =  new Notification.Builder(ctx)
        .setContentTitle(alert)
        .setContentText("incoming notification")
        .setSmallIcon(R.drawable.new_mail)
        .setLargeIcon(aBitmap)
        .build();

        return notification;
    }

    @Override
    public int getNextId(String alert, Map<String, String> extras) {
        return 1001; // Always update the single notification
    }
}

BasicPushNotificationBuilder

The BasicPushNotificationBuilder is the default handler for incoming push notifications. Notifications generated by this builder use the standard Android Notification layout. The icon is set to the application’s icon by default and the notification subject is set to the application’s name.

If you want to create custom notifications, just extend the BasicPushNotificationBuilder class.

For example, the RichNotificationBuilder creates inbox style notifications for rich push messages by extending the BasicPushNotificationBuilder.

  1. Override the buildNotification() method to create the custom inbox notification if the alert message is a rich push message and the extras is not null.
  2. Override the getNextId() method to return the notification ID.
@Override
public Notification buildNotification(String alert, Map<String, String> extras) {
    // Only build inbox style notification for rich push messages
    if (extras != null && RichPushManager.isRichPushMessage(extras)) {
        return createRichNotification(alert);
    } else {
        return super.buildNotification(alert, extras);
    }
}

@Override
public int getNextId(String alert, Map<String, String> extras) {
    if (extras != null && extras.containsKey(PushReceiver.EXTRA_MESSAGE_ID_KEY)) {
        return INBOX_NOTIFICATION_ID;
    } else {
        return super.getNextId(alert, extras);
    }
}

private Notification createRichNotification(String incomingAlert) {
    Context context = UAirship.shared().getApplicationContext();

    int inboxUnreadCount = RichPushInbox.shared().getUnreadCount();

    // Incoming message is not immediately available because the rich push message
    // needs to be fetched first.
    int totalUnreadCount = inboxUnreadCount + 1;

    Resources res = context.getResources();
    Bitmap largeIcon = BitmapFactory.decodeResource(res, R.drawable.ua_launcher);

    InboxStyle style = new Notification.InboxStyle(
            new Notification.Builder(context)
            .setContentTitle("New messages")
            .setContentText(incomingAlert)
            .setLargeIcon(largeIcon)
            .setSmallIcon(R.drawable.new_mail_icon)
            .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
            .setNumber(totalUnreadCount));

    // Add the incoming alert as the first line in bold
    style.addLine(Html.fromHtml("<b>"+incomingAlert+"</b>"));

    // Add summary about remaining unread messages.
    style.setSummaryText("+" + inboxUnreadCount + " more messages");

    return style.build();
}

Once ready to use the custom push notification for rich push messages, assign it to the PushManager.

public class RichPushApplication extends Application {

    @Override
    public void onCreate() {
        UAirship.takeOff(this);
        PushManager.shared().setIntentReceiver(PushReceiver.class);
        RichPushManager.setJavascriptInterface(RichPushMessageJavaScript.class, "urbanairship");

        // If running on Jelly Bean or higher, then use the inbox style notification builder
        if (Build.VERSION.SDK_INT >= 16) {
            PushManager.shared().setNotificationBuilder(new RichNotificationBuilder());
        }
    }
}

CustomPushNotificationBuilder

Push is designed to work out of the box with the basic notification layout used for all system notifications, displaying an icon and brief message. To customize the layout, a PushNotificationBuilder interface is provided.

Here is an example of adding a custom builder to the app, extending the main application class above:

import android.app.Application;
import android.net.Uri;

import com.urbanairship.Logger;
import com.urbanairship.UAirship;
import com.urbanairship.push.CustomPushNotificationBuilder;
import com.urbanairship.push.PushManager;
import com.urbanairship.push.PushPreferences;

public class MyApplication extends Application {

    @Override
    public void onCreate() {

        UAirship.takeOff(this, options);

        CustomPushNotificationBuilder nb = new CustomPushNotificationBuilder();

        nb.statusBarIconDrawableId = R.drawable.icon;

        nb.layout = R.layout.notification_layout; // The layout resource to use
        nb.layoutIconDrawableId = R.drawable.notification_icon; // The icon you want to display
        nb.layoutIconId = R.id.icon; // The icon's layout 'id'
        nb.layoutSubjectId = R.id.subject; // The id for the 'subject' field
        nb.layoutMessageId = R.id.message; // The id for the 'message' field

        //set this ID to a value > 0 if you want a new notification to replace the previous one
        //nb.constantNotificationId = 100;

        //set this if you want a custom sound to play
        nb.soundUri = Uri.parse("android.resource://"+this.getPackageName()+"/" +R.raw.cat);

        // Set the builder
        PushManager.shared().setNotificationBuilder(nb);

    }

}

First, we create a new instance of CustomPushNotificationBuilder, which is distributed as part of the Android client library, and assign layout properties taken from the custom layout notification_layout.xml, found in the Push Sample application.

Custom sounds can be played when a notification is received using this notification builder implementation. For the purposes of this example, we’ll use a cat.mp3 located at project_root/res/raw/cat.mp3. Also you may want to avoid .wav files, as some devices can’t handle them well.

Once we’ve set the necessary properties, we need to assign it to the PushManager.

To configure custom layouts, create a custom layout XML file. It must contain three fields:

  • icon - The icon to display

  • subject - The application name is displayed here

  • message - The alert is displayed here.

    These are all required elements. If you do not wish to use one of the fields, set its visibility to gone.

Here is a closer look at the layout used in this instance:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:orientation="vertical"
    android:paddingTop="2dip"
    android:layout_alignParentTop="true"
    android:layout_height="fill_parent">

  <ImageView android:id="@+id/icon"
      android:src="@drawable/icon_1"
      android:layout_marginRight="4dip"
      android:layout_marginLeft="5dip"
      android:layout_width="100dip"
      android:layout_height="100dip" />

  <!-- The custom notification requires a subject field.
  To maximize space in this layout this
  field is hidden. Visibility is set to gone. -->
  <TextView android:id="@+id/subject"
      android:text="Subject"
      android:layout_alignTop="@+id/icon"
      android:layout_toRightOf="@+id/icon"
      android:layout_height="wrap_content"
      android:layout_width="wrap_content"
      android:maxLines="1" android:visibility="gone"/>

  <!-- The message block. Standard text size is 14dip
  but is increased here to maximize impact. -->
  <TextView android:id="@+id/message"
      android:textSize="48dip"
      android:textColor="#FF000000"
      android:text="Message"
      android:maxLines="4"
      android:layout_marginTop="0dip"
      android:layout_marginRight="2dip"
      android:layout_marginLeft="0dip"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/icon"
      android:layout_width="wrap_content" />

</RelativeLayout>

Note that if you are targeting Honeycomb (3.0) or higher, you will need to take advantage of text styles made available in API level 9 in order to maintain forward compatibility. For more on this topic please read the linked Support Center article. Following this, a forward-compatible variant of this layout would be structured as follows:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:orientation="vertical"
    android:paddingTop="0dip"
    android:layout_alignParentTop="true"
    android:layout_height="fill_parent">

  <ImageView android:id="@+id/icon"
      android:src="@drawable/icon"
      android:layout_width="65dip"
      android:layout_height="65dip"
      android:layout_margin="0dip"/>

<!-- The custom notification requires a subject field.
To maximize space in this layout this
field is hidden. Visibility is set to gone. -->
<TextView android:id="@+id/subject"
    android:text="Subject"
    android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
    android:layout_alignTop="@+id/icon"
    android:layout_toRightOf="@+id/icon"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:maxLines="1" android:visibility="gone"/>

<!-- The message block. Standard text size is 14dip
but is increased here to maximize impact. -->
<TextView android:id="@+id/message"
    android:textSize="48dip"
    android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
    android:text="Message"
    android:maxLines="4"
    android:layout_marginTop="0dip"
    android:layout_marginRight="2dip"
    android:layout_marginLeft="10dip"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@+id/icon"
    android:layout_width="wrap_content" />

</RelativeLayout>

If you have specific needs with respect to custom layouts that are not covered by the CustomPushNotificationBuilder class, you can create your own implementation of the PushNotificationBuilder interface, as specified in the client library Javadoc.

Custom Handling of Push Events

If you want to receive push, push opened and registration intents, you must add a few items to your project.

First, create a broadcast receiver for handling PushManager.ACTION_PUSH_RECEIVED , PushManager.ACTION_NOTIFICATION_OPENED , and PushManager.ACTION_REGISTRATION_FINISHED:

public class IntentReceiver extends BroadcastReceiver {

    private static final String logTag = "PushSample";

    // A set of actions that launch activities when a push is opened.  Update
    // with any custom actions that also start activities when a push is opened.
    private static String[] ACTIVITY_ACTIONS = new String[] {
        DeepLinkAction.DEFAULT_REGISTRY_NAME,
        OpenExternalUrlAction.DEFAULT_REGISTRY_NAME,
        LandingPageAction.DEFAULT_REGISTRY_NAME
    };

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(logTag, "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(logTag, "Received push notification. Alert: "
                    + intent.getStringExtra(PushManager.EXTRA_ALERT)
                    + " [NotificationID="+id+"]");

            logPushExtras(intent);

        } else if (action.equals(PushManager.ACTION_NOTIFICATION_OPENED)) {

            Log.i(logTag, "User clicked notification. Message: " + intent.getStringExtra(PushManager.EXTRA_ALERT));

            logPushExtras(intent);

            // Only launch the main activity if the payload does not contain any
            // actions that might have already opened an activity
            if (!ActionUtils.containsRegisteredActions(intent.getExtras(), ACTIVITY_ACTIONS)) {
                Intent launch = new Intent(Intent.ACTION_MAIN);
                launch.setClass(context, MainActivity.class);
                launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(launch);
            }


            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(logTag, "Registration complete. APID:" + intent.getStringExtra(PushManager.EXTRA_APID)
                    + ". Valid: " + intent.getBooleanExtra(PushManager.EXTRA_REGISTRATION_VALID, false));
        }
    }

    /**
     * Log the values sent in the payload's "extra" dictionary.
     *
     * @param intent A PushManager.ACTION_NOTIFICATION_OPENED or ACTION_PUSH_RECEIVED intent.
     */
    private void logPushExtras(Intent intent) {
        Set<String> keys = intent.getExtras().keySet();
        for (String key : keys) {

            //ignore standard extra keys (GCM + UA)
            List<String> ignoredKeys = (List<String>)Arrays.asList(
                    "collapse_key",//GCM collapse key
                    "from",//GCM sender
                    PushManager.EXTRA_NOTIFICATION_ID,//int id of generated notification (ACTION_PUSH_RECEIVED only)
                    PushManager.EXTRA_PUSH_ID,//internal UA push id
                    PushManager.EXTRA_ALERT);//ignore alert
            if (ignoredKeys.contains(key)) {
                continue;
            }
            Log.i(logTag, "Push Notification Extra: ["+key+" : " + intent.getStringExtra(key) + "]");
        }
    }
}

Next, add the receiver to the manifest:

<receiver android:name="com.urbanairship.push.sample.IntentReceiver" />

Finally, register the receiver in your application’s onCreate method after takeOff:

PushManager.shared().setIntentReceiver(IntentReceiver.class);

Clearing Notifications

Notifications can be cleared manually by using the Android NotificationManager cancelAll() method.

3.0+ Compatibility for Custom Notification Layouts

The new Android client library allows you to extensively customize the way notifications are displayed, by incorporating custom layouts. However, with this flexibility also comes the responsibility of making sure that your notification layout displays correctly on different devices and Android OS version, in particular, Android’s recent Honeycomb release for tablets, and the 4.x Ice Cream Sandwich and Jellybean releases.

Here are a few potential pitfalls you can avoid when incorporating custom notification layouts into an app that targets 3.0 (Honeycomb)+.

Invisible Message Text

In the official Android Creating Status Bar Notifications guide, the example layout file defines a TextView with a black textColor attribute for the notification message.

This was sound advice when the default background for the expanded status bar window was white or gray, but in Honeycomb, this window has been replaced with new redesigned notification UI with a black background. This means that if you create a layout similar to the one shown in the guide linked above, your notification messages will appear to be blank.

The solution is to use a pair of text styles introduced in API level 9 that define the default colors for status bar events, which will allow your layout to adapt to changing UI themes:

@android:style/TextAppearance.StatusBar.EventContent.Title

and

@android:style/TextAppearance.StatusBar.EventContent

On Honeycomb, these result in white and light gray text, respectively, and match the style of system notifications that tablet users will already be accustomed to.

Because these styles were added in API level 9, you must build your application targeting at least this version in order to take advantage of them. If you want your app to run on devices with an earlier Android release, you can set an earlier API level as your minimum SDK version. This will generate a compiler warning, but as long as you refrain from making incompatible API calls (or only do so after performing reflection in order to determine if it is safe to do so), your app will run fine on earlier devices.

In this case you will need to create two layouts– one using the “traditional” text color attribute, and another using the Honeycomb-compatible styles referenced above. Place the new layout in its own directory, named “layout-v9”, and Android will know to look for it there when the device is running API level 9 or above.

Structure

Our Honeycomb-compatible purchase notification layout would be structured like the example below. You will need to take advantage of text styles made available in API level 9 in order to maintain forward compatibility.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:padding="5dp"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <ImageView
                android:id="@+id/status_icon"
                android:layout_width="wrap_content"
                android:layout_height="fill_parent"
                android:layout_alignParentLeft="true"
        />
        <RelativeLayout
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:paddingLeft="10px"
                android:layout_toRightOf="@id/status_icon">
                <TextView
                        android:id="@+id/status_text"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content"
                        android:layout_alignParentTop="true"
                        android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
                        android:textSize="14sp"
                        android:textStyle="bold"
                />
                <FrameLayout
                        android:id="@+id/status_progress_wrapper"
                        android:layout_height="wrap_content"
                        android:layout_below="@id/status_text"
                        android:layout_width="fill_parent">
                        <ProgressBar
                                android:id="@+id/status_progress"
                                android:layout_height="wrap_content"
                                android:layout_below="@id/status_text"
                                android:progressDrawable="@android:drawable/progress_horizontal"
                                android:indeterminate="false"
                                android:indeterminateOnly="false" android:layout_width="fill_parent"/>
                </FrameLayout>

                <FrameLayout
                        android:id="@+id/status_wheel_wrapper"
                        android:layout_height="wrap_content"
                        android:layout_below="@id/status_text"
                        android:layout_width="wrap_content" android:layout_centerInParent="true"
                        android:visibility="gone">
                        <ProgressBar
                                android:id="@+id/status_wheel"
                                android:layout_height="30px"
                                android:layout_below="@id/status_text"
                                android:indeterminate="true"
                                android:indeterminateOnly="true"
                                android:layout_width="30px" android:layout_centerInParent="true"
                                style="@android:style/Widget.ProgressBar.Small"/>
                </FrameLayout>
        </RelativeLayout>
</RelativeLayout>

Persistence of Ticker Text

The Android Notification class defines a flag FLAG_ONLY_ALERT_ONCE, which the documentation says “should be set if you want the sound and/or vibration play each time the notification is sent, even if it has not been canceled before that.” As this interpretation is the exact opposite of the name of the flag, it is likely that this is a mistake in the documentation. Furthermore, in prior Android releases, setting this flag does not appear to have any noticeable effect.

In Honeycomb, however, if you do not set this flag and your notification is ongoing or involves UI elements that will be periodically updated, such as a progress bar, every update to the existing notification will cause the ticker test to redisplay, which will make the notification itself inaccessible. Therefore, if you are creating your own implementation of the PushNotificationBuilder and intend on updating the notification UI in any way after it has been displayed, make sure to explicitly set this flag on the notification returned.

Other Resources

Backwards compatibility and cross-device support are complex topics that require careful planning and carry their own set of best practices. This guide is meant to help you prevent some known issues from arising with custom notifications, but is in no way an exhaustive resource. For more information on how to design your app to maximize compatibility, see the following links:

Creating Backward Compatible UIs

Status Bar Icons

Android Developers Forum Google Groups

Widgets

Android widgets provides a way to display a view of the app to the user on the homescreen or keyguard. When building widgets, you have full access to the UrbanAirship library, including access to the app’s rich push inbox.

The following example code shows how to create a home screen widget with the current unread rich push message count. A more complete example of showing a list of inbox messages can be found in the Rich Push Sample application.

First, define the widget’s layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#232323"
    android:layout_margin="@dimen/widget_margin" >

    <LinearLayout
        android:id="@+id/widget_header"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:orientation="horizontal" >

        <ImageView
            android:id="@+id/widget_title_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="12dp"
            android:scaleType="fitStart"
            android:adjustViewBounds="true"
            android:src="@drawable/ua_launcher" />

        <TextView
            android:id="@+id/widget_header_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textAllCaps="true"
            android:textColor="#F8F8F8"
            android:textSize="24sp" />
    </LinearLayout>
</LinearLayout>

Create the widget’s provider:

public class WidgetProvider extends AppWidgetProvider {
      public static String REFRESH_ACTION = "com.example.sample.widget.REFRESH";

      private static HandlerThread workerThread;
      private static Handler workerQueue;

      public WidgetProvider() {
          // Start the worker thread
          workerThread = new HandlerThread("RichPushSampleInbox-Provider");
          workerThread.start();
          workerQueue = new Handler(workerThread.getLooper());
      }

      @Override
      public void onReceive(final Context context, Intent intent) {

          // If we receive a refresh action, then force an update on the worker thread
          if (intent.getAction().equals(REFRESH_ACTION)) {
              workerQueue.removeMessages(0);
              workerQueue.post(new Runnable() {
                  @Override
                  public void run() {
                      AppWidgetManager mgr = AppWidgetManager.getInstance(context);
                      ComponentName cn = new ComponentName(context, RichPushWidgetProvider.class);
                      onUpdate(context, mgr, mgr.getAppWidgetIds(cn));
                  }
              });
          }

          super.onReceive(context, intent);
      }

      @Override
      public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

          // Update each widget based on their id
          for (int id : appWidgetIds) {

            // Create a new remote view for the widget
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

            // Get the current unread count from the rich push inbox
            int count = RichPushManager.shared().getRichPushUser().getInbox().getUnreadCount();

            // Update the header for the current unread message count
            remoteViews.setTextViewText(R.id.widget_header_text, "Unread messages: " + count);

            // Add a click pending intent to launch the application
            Intent intent = new Intent(context, MainActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.widget_header, pendingIntent);

            // Update the widget view
            appWidgetManager.updateAppWidget(id, remoteViews);
          }

          super.onUpdate(context, appWidgetManager, appWidgetIds);
      }
}

Create the widget’s info in the xml resource directory:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="1800000"
    android:initialLayout="@layout/widget_layout"
    android:previewImage="@drawable/widget_preview">
</appwidget-provider>

Update the AndroidManifest.xml with the new widget provider and widget info:

<receiver android:name=".widget.WidgetProvider">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <!-- This specifies the widget provider info -->
    <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_info" />
</receiver>

The widget will only refresh its content every 1800000 ms (defined in the widget’s info). The unread count could update outside of this update period, so to force the widget to update run the following code:

Intent refreshIntent = new Intent(context, WidgetProvider.class);
refreshIntent.setAction(WidgetProvider.REFRESH_ACTION);
context.sendBroadcast(refreshIntent);

Apple, StoreKit and iPhone are trademarks of Apple, Inc. Maponics Neighborhood Boundaries © Maponics 2012. DMA® is a registered service mark of The Nielsen Company. Used under License.