Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Triggering Data Synchronization From the Cloud Using Urban Airship Push Notifications

DZone's Guide to

Triggering Data Synchronization From the Cloud Using Urban Airship Push Notifications

Learn how to use an Urban Airship push notification to trigger data synchronization from the cloud to a mobile device with this step-by-step tutorial.

· Mobile Zone
Free Resource

Launching an app doesn’t need to be daunting. Whether you’re just getting started or need a refresher on mobile app testing best practices, this guide is your resource! Brought to you in partnership with Perfecto

In this post, I’ll show a sample Android application built with Couchbase Lite that uses an Urban Airship push notification to trigger synchronizing data from the cloud to a mobile device. Here “the cloud” will be a Sync Gateway instance setup in a test configuration (Couchbase Lite and Sync Gateway are both part of the Couchbase Mobile stack. You can read about Couchbase Mobile here).

Background

Couchbase Mobile synchronizes data using unidirectional replications. You can set replications to run and end immediately once the local data is up-to-date (“one-shot”), or to listen indefinitely for new changes (“continuous”). There are a few reasons you might want to use a service like Urban Airship to trigger synchronization instead of setting up a continuous replication. For example, a continuous “pull” replication (one that retrieves data from the cloud) needs to keep a network connection open. An open connection will necessarily cause some amount of drain on the device battery, even while inactive. Ideally, an application would only open a connection when there are changes to transfer.

Beyond that, push notification services like Urban Airship typically have a number of advanced features for addressing groups of devices. UA can give you great flexibility in managing when operations happen. Furthermore, if your app already uses a push service, that already requires its own open network connection. It makes sense to use that to trigger Couchbase Lite instead of doubling up the networking.

This example will show how to integrate Urban Airship. I’ll walk through all the necessary pieces, so you can build a fully working application.

What You’ll Need

  • An Urban Airship account
  • A Firebase account (requires a Google account).
  • Android Studio or the equivalent.

Note: You can read about setting up Urban Airship in this blog post.

The Application

Here’s a look at the application in action. The upper part of the image shows the app running. The lower part shows a push notification sent from the command line. When the app receives the push notification, it puts up a progress spinner, fires off a one-shot pull replication, then displays the document changes received in a text view.


To understand this example, we’ll take a look at four classes, some of the “glue” that brings them together, and show how to test the results.

Classes Required by Urban Airship

To use Urban Airship (UA), we need to implement two classes, a subclass of Autopilot, and a subclass of AirshipReceiver.

Autopilot

UA has to be initialized before use. You can do that in two ways; either by calling takeOff in the application’s onCreate method or by creating a subclass of Autopilot. I chose to do the latter. As we’ll see, UA has an interesting way of automatically using our subclass.

The class is simple. I don’t want to display anything in the notification bar. I use this class to turn off user notifications, and that’s all. Here’s the full listing of CBAutoPilot.java:

package com.couchbase.cblite.android.cbpushsync;

import com.urbanairship.Autopilot;
import com.urbanairship.UAirship;

public class CBAutoPilot extends Autopilot {
    @Override
    public void onAirshipReady(UAirship airship) {
        airship.getPushManager().setUserNotificationsEnabled(false);
    }
}

To use this class, UA employs a technique unique to Android. You indicate which class to use with an entry in your Android manifest file. For my case, the entry looks like this (placed inside the application section):

<meta-data android:name="com.urbanairship.autopilot"
            android:value="com.couchbase.cblite.android.cbpushsync.CBAutoPilot"/>

This is where a bit of Android magic happens. UA includes a Content Provider class to automate the initialization. Content Providers are guaranteed to get instantiated before the application’s onCreate method gets called. UA uses that to include code to read this meta-data entry, instantiate the subclass, and call the onAirshipReady method.

AirshipReceiver

With UA initialized and ready to go, we need to provide the hooks to capture and process notifications. To do that, implement a subclass of AirshipReceiver. Again, the class is straightforward. Here’s the full listing of CBAirshipReceiver.java:

package com.couchbase.cblite.android.cbpushsync;

import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;

import com.urbanairship.AirshipReceiver;
import com.urbanairship.push.PushMessage;

public class CBAirshipReceiver extends AirshipReceiver {
    private static final String TAG = "CBAirshipReceiver";

    @Override
    protected void onPushReceived(Context context, PushMessage message, boolean notificationPosted) {
        Log.i(TAG, "Received push message. Alert: " + message.getAlert() + ". posted notification: " + notificationPosted);

        switch(message.getAlert()) {
            case "start":
                CBLHelper.getInstance().startReplication();
                break;
            case "stop":
                CBLHelper.getInstance().stopReplication();
                break;
            case "reset":
                CBLHelper.getInstance().reset();
                break;
            default:
                break;
        }
    }

    @Override
    protected void onChannelCreated(@NonNull Context context, @NonNull String channelId) {
        Log.i(TAG, "Channel created. Channel Id:" + channelId + ".");
    }

    @Override
    protected void onChannelUpdated(@NonNull Context context, @NonNull String channelId) {
        Log.i(TAG, "Channel updated. Channel Id:" + channelId + ".");
    }

    @Override
    protected void onChannelRegistrationFailed(Context context) {
        Log.i(TAG, "Channel registration failed.");
    }
}

When we get to testing the app- we’ll see how UA lets you set the alert text. I used that here to provide a few different remote operations. An alert can start and stop replications. I also included the ability, by sending “reset,” to delete the database. I did that because once you replicate the data, it’s persistent on the device. Shutting down the app doesn’t reset the local database. Rather than always having to add new documents through Sync Gateway to show replication working, I added the ability to wipe the device database and start over.

You’ll notice a few other overridden methods above. Those methods are all declared abstract in the AirshipReceiver class, so we have to include at least a minimal implementation. It turns out, though, that we need to know the device channel ID to send it a message. This channel ID changes often. In a future post, I’ll show how I register the ID so a server-side application can send to the device. For now, it’s useful to log the ID so we can extract it and use it in testing.

The Application Classes

The hooks above get UA up and running, trap notifications, and trigger actions based on their contents. Now let’s take a look at the application itself. The app consists of one activity set up so we can see the contents of documents as they get pulled. The other class wraps some of the Couchbase Lite functionality in a helper.

The Main Activity

In the main activity, I want to show a progress bar while replications happen, then display the set of documents sent. Here’s the full listing of MainActivity.java:

package com.couchbase.cblite.android.cbpushsync;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.couchbase.lite.Database;
import com.couchbase.lite.Document;
import com.couchbase.lite.DocumentChange;
import com.couchbase.lite.replicator.Replication;
import com.couchbase.lite.replicator.ReplicationState;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {
    private static String TAG = MainActivity.class.getSimpleName();
    private static final ObjectMapper mapper = new ObjectMapper();

    private CBLHelper cblHelper = CBLHelper.getInstance();
    private TextView documentView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        documentView = (TextView) findViewById(R.id.documentView);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);

        cblHelper.initialize(
            new Database.ChangeListener() {
                @Override
                public void changed(Database.ChangeEvent event) {
                    Log.i(TAG, "Database change called: count - " + event.getChanges().size());

                    if (!event.isExternal()) {
                        return;
                    }

                    String json = "";

                    for (final DocumentChange change : event.getChanges()) {
                        if (!change.isCurrentRevision()) {
                            continue;
                        }

                        Document changedDoc = cblHelper.getExistingDocument(change.getDocumentId());

                        if (changedDoc == null) continue;

                        try {
                            json += mapper.writeValueAsString(changedDoc.getProperties());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    final String text = json;

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            documentView.setText(text);
                        }
                    });
                }
            },
            new Replication.ChangeListener() {
                @Override
                public void changed(Replication.ChangeEvent event) {
                    Log.i(TAG, "Replication change called: " + event.toString());

                    if (event.getError() != null) return;

                    if (event.getTransition() == null) return;

                    ReplicationState dest = event.getTransition().getDestination();

                    final int replicationProgress =
                            ((dest == ReplicationState.STOPPING ||
                              dest == ReplicationState.STOPPED) ?
                                    View.INVISIBLE : View.VISIBLE);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            progressBar.setVisibility(replicationProgress);
                        }
                    });
                }
            }
        );
    }
}

The core of the code here consists of the two change listeners, one for the database, and one for replications. Each listener interface defines a single method changed.

The Document changes listener lets us track the actual document updates. A replication may change more than one document at a time. The change event returns a list of document IDs. Looping over the document IDs, we retrieve each document directly, translate the JSON contents into a string, and append it to the text to display. The change listeners are called on a background thread. To finish out, we need to manipulate the UI elements on the main thread. This is easily done with the runOnUiThread method of the Activity. The other listener allows us to display a busy-wait spinner (indefinite progress bar) depending on the Replication state. You can read more about monitoring replication state here.

The Database Helper Class

I often wrap database functions in a helper class. Since we only need one instance of the helper, I use a singleton pattern. Just like UA, I want the helper available before the main application starts. I use the same technique of putting the code in a Content Provider. Here’s the full listing of CBLHelper.java:

package com.couchbase.cblite.android.cbpushsync;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.couchbase.lite.CouchbaseLiteException;
import com.couchbase.lite.Database;
import com.couchbase.lite.Document;
import com.couchbase.lite.Manager;
import com.couchbase.lite.android.AndroidContext;
import com.couchbase.lite.replicator.Replication;

import java.io.IOException;
import java.net.URL;

public class CBLHelper extends ContentProvider {
    public static final String TAG = "Push Sync";

    private static String DB = "db";
    //private static String syncGateway = "http://localhost:4984";
    private static String syncGateway = "http://10.0.2.2:4984";
    private static URL syncGatewayURL;

    private Manager manager;
    private Database database;
    private Replication puller;

    private Database.ChangeListener databaseChangeListener;
    private Replication.ChangeListener replicationChangeListener;

    private static CBLHelper instance;

    public static CBLHelper getInstance() { return instance; }

    @Override
    public boolean onCreate() {
        instance = this;

        enableLogging();

        try {
            syncGatewayURL = new URL(syncGateway + "/" + DB);
            manager = new Manager(new AndroidContext(getContext()), Manager.DEFAULT_OPTIONS);
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        return true;
    }

    public void closeDatabase() {
        database.close();
    }

    public void startReplication() {
        puller.start();
    }

    public void stopReplication() { puller.stop(); }

    public void reset() {
        stopReplication();

        try {
            database.delete();
        } catch (CouchbaseLiteException ex) {
            ex.printStackTrace();
        }

        initialize(databaseChangeListener, replicationChangeListener);
    }

    public void initialize(Database.ChangeListener dbl, Replication.ChangeListener rl) {
        try {
            database = manager.getDatabase(DB);
        } catch (CouchbaseLiteException ex) {
            ex.printStackTrace();
        }

        database.addChangeListener(dbl);
        databaseChangeListener = dbl;

        puller = database.createPullReplication(syncGatewayURL);
        puller.addChangeListener(rl);
        replicationChangeListener = rl;
    }

    public Document getExistingDocument(String documentID) {
        return database.getExistingDocument(documentID);
    }

    // Logging

    private void enableLogging() {
        Manager.enableLogging(TAG, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG_SYNC_ASYNC_TASK, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG_SYNC, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG_QUERY, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG_VIEW, Log.VERBOSE);
        Manager.enableLogging(com.couchbase.lite.util.Log.TAG_DATABASE, Log.VERBOSE);
    }

    // Helper function to dispatch on UI thread
    private void runOnUiThread(Runnable runnable) {
        new Handler(Looper.getMainLooper()).post(runnable);
    }

    // Required overrides

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

You can see this class just forms a thin wrapper. We retain an instance of the class during onCreate which becomes our singleton. Some other basic setup happens, like enabling logging and creating a database manager. I wrote an initialize method used to actually open the database, prepare the pull replication, and attach the two change listeners. Recall this is called from our Activity class. (Note: You can have more than one change listener for each type. Couchbase keeps a list of all added listeners, not just the latest one.) The other database methods just provide simple shortcuts.

A content provider has several mandatory methods to implement. We need to provide stubs for those, which you see in the last five methods.

The Android Manifest

Finally, just to show how this ties together and the various parts get exposed, I’ve included the Android manifest file for the project. Here’s the complete listing of AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.couchbase.cblite.android.cbpushsync">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <meta-data android:name="com.urbanairship.autopilot"
            android:value="com.couchbase.cblite.android.cbpushsync.CBAutoPilot"/>
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".CBAirshipReceiver"
            android:exported="false">

            <intent-filter>
                <action android:name="com.urbanairship.push.CHANNEL_UPDATED" />
                <action android:name="com.urbanairship.push.OPENED" />
                <action android:name="com.urbanairship.push.RECEIVED" />
                <action android:name="com.urbanairship.push.DISMISSED" />

                <category android:name="${applicationId}" />
            </intent-filter>
        </receiver>
        <provider
            android:authorities="${applicationId}.dbhelper"
            android:exported="false"
            android:enabled="true"
            android:name=".CBLHelper" />
    </application>

</manifest>

The UA library comes with its own manifest file. The Android build system integrates all the different manifest files to create the final one. The UA library manifest file is where you’ll find the entries for the Content Provider that bootstraps the library.

Sync Gateway

To try the app out, I used Sync Gateway with the built-in walrus database. Walrus is an in-memory database usually used just for testing. This saves the trouble of setting up a back-end Couchbase Server instance. Here’s the complete listing of the Sync Gateway configuration file:

{
  "log": ["HTTP+"],
  "adminInterface": "127.0.0.1:4985",
  "interface": "127.0.0.1:4984",
  "CORS": {
    "origin":["*"],
    "loginorigin":["*"],
    "headers":["Content-Type"],
    "maxAge": 1728000
  },
  "databases": {
    "db": {
      "server": "walrus:",
      "users": { "GUEST": {"disabled": false, "admin_channels": ["*"] } }
    }
  }
}

This tells Sync Gateway to listen only on the localhost interface, accept any cross origin requests, and enables the special GUEST with access to all channels. It’s a pretty good general purpose configuration to start with to make sure everything’s working.

You might have noticed we use 10.0.2.2 as the IP address for Sync Gateway in the Android app. The standard emulator that comes with Android Studio maps this automatically to the hosting machine’s localhost. If you use a different emulator (Genymotion is another popular one), be sure to look up what IP address to use, since they differ.

If you want to learn more about setting up Sync Gateway on your development machine, or how to work with it from the command line, take a look at this blog series.

Testing

To test everything and record the animation shown at the beginning of the blog, I run both the Android emulator and Sync Gateway on one machine. I then post push notifications to UA through their REST API.

To get something interesting prepared, I first add a document to Sync Gateway. Here’s a shell command to do that.

$ curl -X PUT -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{ "test" : "UA", "channels": "public" }' http://localhost:4984/db/doc

(Read more about this here).

With a new document in the database on Sync Gateway, and the app up and running, all that’s left is to trigger a pull replication to see the action.

Here’s the curl command to send a “start” signal. Recall I pull the value from the “alert” part of the notification to trigger actions.

$ curl https://go.urbanairship.com/api/push -u 'appKey:appMasterSecret' -X POST -H 'Content-Type: application/json' -H 'Accept: application/vnd.urbanairship+json; version=3;' -d '{ "audience": { "android_channel": "3110bc81-1c7b-4ef8-a7a3-7c56aab19f08" }, "notification" : { "alert": "start" }, "device_types": "all" }'

You’ll need to provide your own application key and application master secret above. You also need to look at the log files to see the channel value to enter for “android_channel.” That changes fairly regularly. Unfortunately, UA doesn’t do much to indicate this. If you find push notifications don’t seem to be having any effect, check the channel value.

After posting to UA, you should see a response like this.

{"ok":true,"operation_id":"2a6c0edf-e5d0-4b4d-b762-ad5c4478c291","push_ids":["1951019a-2b7f-4f1b-8888-e87047a1feec"],"message_ids":[],"content_urls":[]}

And, finally, to reset to a blank database to try again, you can post this.

$ curl https://go.urbanairship.com/api/push -u 'appKey:appMasterSecret' -X POST -H 'Content-Type: application/json' -H 'Accept: application/vnd.urbanairship+json; version=3;' -d '{ "audience": { "android_channel": "3110bc81-1c7b-4ef8-a7a3-7c56aab19f08" }, "notification" : { "alert": "reset" }, "device_types": "all" }'

That’s it for getting our Android app prepared to perform replications based on push notifications. Be sure and check out this post on monitoring the Sync Gateway changes feed. I’ll be writing another post to show a more sophisticated version, combining monitoring the changes feed with triggering client actions soon.

Keep up with the latest DevTest Jargon with the latest Mobile DevTest Dictionary. Brought to you in partnership with Perfecto.

Topics:
mobile ,push notifications ,couchbase lite ,urban airship ,mobile app development

Published at DZone with permission of Hod Greeley. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}