Friday, January 22, 2016

Handling Parse push notifications in a specific Activity

Intro

In a previous tutorial I explained how you can use Parse.com to send a notification, and how to make your app receive the notification properly.
The final status of that example configured Gradle to install the Parse SDK, along with setting up the manifest file to accept the notification, and some java coding so the app would not crash when a notification arrived. With this completed, you were able to send a notification from Parse.com and your app will receive it and it will open the app in whatever was your initial activity.
This new tutorial takes you a step beyond.

The purpose of this tutorial

While receiving a push notification and opening your app without a crash is definitely something to cheer about, the practical usage of this may be limited.
In this tutorial we will build on the app created on the previous tutorial, and we will assume the need is now to display the notification in a "Message details" activity, which will display the title and body of the message.
For this scenario, we will create the following activity flow:
Homepage -> Account -> Inbox -> Message details
As you can see, just opening the app in the homepage won't cut it anymore.

Let's start coding!

The first thing will be creating this workflow. You can make it as pretty as you want, but since UI is not the purpose of this tutorial, I will keep it as ugly basic as possible so we can focus on the specific of the notification.

Note: as usual, the entire final code of this tutorial can be found here.

Create activities

If you're reading this tutorial, you should already know how to do this. Just to stay in sync, here's what I did:

Main activity.java (basically whatever the sample app has, plus the button)
public class MainActivity extends AppCompatActivity {

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

        Button buttonToSecondActivity = (Button)findViewById(R.id.button_start_second_activity);
        buttonToSecondActivity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });
    }
}
SecondActivity.java
public class SecondActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_activity);


        Button buttonToInbox = (Button)findViewById(R.id.button_start_inbox);
        buttonToInbox.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(SecondActivity.this, InboxActivity.class);
                startActivity(intent);
            }
        });
    }
}
InboxActivity.java
public class InboxActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.inbox_activity);


        Button buttonToMessageDetails = (Button)findViewById(R.id.button_message_details);
        buttonToMessageDetails.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(InboxActivity.this, MessageDetails.class);
                startActivity(intent);
            }
        });
    }
}
MessageDetails.java
public class MessageDetails extends Activity {

    TextView messageDetailsTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.message_details_activity);
    }
}

And we add the new activities to the Manifest.xml.
At this point now, the app does this:


And as you can see, it has nothing to do with the push notification yet.

Subclass the notification receiver

So the way a notification works is the app sets up a receiver, and this receiver is running all the time at the OS level even if the app is not running. This is what makes a push notification arrive even when you haven't opened the app in the last month. As long as the app is installed, the receiver will be running.
The "problem" we have with Parse is that parse comes with its own receiver and makes the process of making the receiver (a painful process I should add) almost invisible, as it just works. I say this is a problem because you can receive notifications without ever knowing you needed a receiver or how it works, but now you need to get a crash course on it. (someday you may want to learn how to do this without Parse)

But since the process of making a receiver is tedious, and we are already using Parse with a valid receiver, all we would need to do then is modify the receiver Parse gives us so it can do exactly what we want. To do this, we create a new class and we subclass ParsePushBroadcastReceiver. I will call my new class MyCustomReceiver.

import com.parse.ParsePushBroadcastReceiver;

import android.content.Context;
import android.content.Intent;

/**
 * @author Eduardo Flores
 */
public class MyCustomReceiver extends ParsePushBroadcastReceiver
{
    // TODO
}

Now with the ParsePushBroadcastReceiver subclassed, we can actually do several things, but what we are looking for is this: Once the user pushes the notification in the notification bar of the phone, we want the app to start on the MessageDetails class displaying the data from the notification.
To do this, we will override the method onPushOpen, to make our MyCustomReceiver look like this so far:

import com.parse.ParsePushBroadcastReceiver;

import android.content.Context;
import android.content.Intent;

/**
 * @author Eduardo Flores
 */
public class MyCustomReceiver extends ParsePushBroadcastReceiver
{
    @Override
    protected void onPushOpen(Context context, Intent intent) {
        super.onPushOpen(context, intent);
    }
}
Again, so far we have accomplished nothing new, but we're getting somewhere.
The onPushOpen method contains 2 parameters: a Context and an Intent. The context is useful so we can call things within the app before the app opens, and the Intent is what actually contains our notification data. This intent contains the data as a JSONObject.
But instead of parsing data and handling what it has or doesn't have, we will just start a new activity and just send the whole Intent along the ride. It will be the job of the MessageDetails class to parse and read the data from the Intent.
So passing the data to the activity, our final version of MyCustomReceiver.java looks like this:
import com.parse.ParsePushBroadcastReceiver;

import android.content.Context;
import android.content.Intent;

/**
 * @author Eduardo Flores
 */
public class MyCustomReceiver extends ParsePushBroadcastReceiver
{
    @Override
    protected void onPushOpen(Context context, Intent intent) {
        super.onPushOpen(context, intent);

        Intent intentToMessage = new Intent(context, MessageDetails.class);
        intentToMessage.putExtras(intent);
        intentToMessage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intentToMessage);
    }
}
To explain what we're doing here:
- We create a new Intent (intentToMessage) with the MessageDetails class as the destination
- We add the Intent from the parameter as an extra, so the MessageDetails receives the Intent of the parameter
- We add a flag as NEW_TASK to our newly created intent. The reason for this is because we want the app to start as new. You can play with different flags if you would like. Different flags have different behavior, particularly with the back button
- We start the activity

And now we're done with our MyCustomReceiver.java class!
However, we're not really calling from anywhere, so we need to fix that.

Add the new MyCustomReceiver to the Manifest

If you've been following along from my previous tutorial, then you have been calling a receiver all along...the Parse receiver!
Now we need to modify the manifest to call the new MyCustomReceiver.
So to make this clear, here's how the now old version of the manifest looks like:
<service android:name="com.parse.PushService" />
<receiver android:name="com.parse.ParsePushBroadcastReceiver"
          android:exported="false">
    <intent-filter>
        <action android:name="com.parse.push.intent.RECEIVE" />
        <action android:name="com.parse.push.intent.DELETE" />
        <action android:name="com.parse.push.intent.OPEN" />
    </intent-filter>
</receiver>
<receiver android:name="com.parse.GcmBroadcastReceiver"
          android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="eduardoflores.com.test_parsepush" />
    </intent-filter>
</receiver>  

And now, the modified version looks like this:
<service android:name="com.parse.PushService" />
<receiver android:name=".MyCustomReceiver"
          android:exported="false">
    <intent-filter>
        <action android:name="com.parse.push.intent.RECEIVE" />
        <action android:name="com.parse.push.intent.DELETE" />
        <action android:name="com.parse.push.intent.OPEN" />
    </intent-filter>
</receiver>
<receiver android:name="com.parse.GcmBroadcastReceiver"
          android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="eduardoflores.com.test_parsepush" />
    </intent-filter>
</receiver>  

Notice the second line in both versions. We changed
from: "android:name="com.parse.ParsePushBroadcastReceiver""
to: "android:name=".MyCustomReceiver""

And now we're officially done with the MyCustomReceiver, and the Manifest!

To the last part...

Read and display the notification

Now we need to work on parsing the message in MessageDetails. Remember, this comes as an intent, but only if there's a notification. The user can also access this activity by just manually going to it (assuming your workflow allows this). We'll work on both cases.

So the data comes from the intent as a string with the key "com.parse.Data", so we need to look for this in the onCreate method, like this:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.message_details_activity);

    getIntent().getExtras().getString("com.parse.Data");
}

This will give us a JSONObject, and since we're getting a JSONObject from the intent, we need to wrap it in a try/catch block.
The second case is when a user comes to this activity (assuming that is allowed) without an intent. Because of this, and other scenarios, we need to check for null objects.
After checking for null, and adding the try/catch block, the onCreate looks like this:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.message_details_activity);

    if (getIntent() != null && 
        getIntent().getExtras() != null && 
        getIntent().getExtras().getString("com.parse.Data") != null)
    {
        try
        {
            JSONObject json = new JSONObject(getIntent().getExtras().getString("com.parse.Data"));
            Log.i("MY_APP", json.toString());
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}
So in here we're checking for intent, we're checking if the intent has extras, and then we're checking if the intent has the com.parse.Data intent. If all of that is correct, we move forward with our data extraction.

One thing: as you notice here there's a log in parsing the JSON. What we're going to receive is a JSON that has a structure, but at the moment we don't necessarily know what that structure is.
Since we're going to receive this notification when the app is closed, adding breakpoints to the intent won't work (since the debugger will be closed). By adding a log we can then see what the JSONObject looks like, and we can plan our parsing appropriately.

Now it is time to send a notification from Parse.com, and see what we get!

Send a notification

With our app setup to read the JSONObject coming from Parse.com, we now need to send a notification and see what we get.

First of all, connect your Android device (or start up the emulator) and run your android app. Once the app is running, close it and wait for the notification to arrive.

Now we're just going to go to Parse.com, into our application and we will send a simple notification just like before.



Once the application arrives, you should see this in the log in Android Studio:
MY_APP: {"alert":"Hello from Parse - number 1","push_hash":"92cet0f18c931447911eoecd675f58a8"}

Yay we got our message!
Pushing on the notification should now open your app in the MessageDetails activity, and although we have not changed much on the look of the message, we now have a working JSONObject to read.

A few things to notice at this point:
- The "alert" key seems to be what Parse uses for their ParsePushBroadcastReceiver to display the message. You can change this by continuing subclassing Parse classes and overriding methods, but for now I will leave it as is
- The "push_hash" key provides a unique identifier for your push notification. As of today (Jan 2016), Parse does not provide an "Inbox" API that contains a list of all the notification sent to an account. That's a bummer. Even Urban Airship does it...

Finally, while this simple message is enough for some basic stuff, we actually want a more complex message. For this, we will send a more specific JSON directly from Parse, like this:



The JSON payload has this information:
{
    "alert": "Test push numero dos!",
    "badge": "Increment",
    "sound": "chime",
    "title": "Hey! You got a new notification!",
    "body": "Hello, this is a test push notification from Parse, coming to you via an android app"
}
Send the notification again, and you should get this in the log:
MY_APP: {"alert":"Test push numero dos!",
"badge":"Increment",
"body":"Hello, this is a test push notification from Parse, coming to you via an android app",
"push_hash":"...",
"sound":"chime",
"title":"Hey! You got a new notification!"}
So now, we can parse the JSONObject, and get what we really want, which in this case is the "body" of the notification, and apply it to a String:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.message_details_activity);

    String messageContent = "NO DATA";
    if (getIntent() != null 
        && getIntent().getExtras() != null && 
        getIntent().getExtras().getString("com.parse.Data") != null)
    {
        try
        {
            JSONObject json = new JSONObject(getIntent().getExtras().getString("com.parse.Data"));
            Log.i("MY_APP", json.toString());
            messageContent = json.getString("body");

        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}
And now we have the "body" element out of the notification!
The last thing I'll do is add the JSON structure to my class as a comment (for further reference), and I will display the body in the app, so I will send the string to a textview. The final result of my entire MessageDetails.java class looks like this:
import org.json.JSONException;
import org.json.JSONObject;

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

/**
 * @author Eduardo Flores
 */
public class MessageDetails extends Activity {

    /*
    Parse is now sending this JSON:

    {
        "alert": "Notification alert text",
        "badge": "Increment",
        "sound": "chime",
        "title": "Hey! You got a new notification!",
        "body": "Hello, this is a test push notification from Parse, coming to you via an android app"
    }
     */
    TextView messageDetailsTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.message_details_activity);

        String messageContent = "NO DATA";
        if (getIntent() != null && 
            getIntent().getExtras() != null && 
            getIntent().getExtras().getString("com.parse.Data") != null)
        {
            try
            {
                JSONObject json = new JSONObject(getIntent().getExtras().getString("com.parse.Data"));
                Log.i("MY_APP", json.toString());
                messageContent = json.getString("body");

            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        messageDetailsTextView = (TextView)findViewById(R.id.tv_message);
        messageDetailsTextView.setText(messageContent);
    }
}

And this is what the app looks like at the end:


The code for this tutorial can be downloaded from here.

No comments:

Post a Comment