Skip to content
This repository has been archived by the owner on Sep 4, 2020. It is now read-only.

Commit

Permalink
Issue #683: Support Android N inline reply actions
Browse files Browse the repository at this point in the history
  • Loading branch information
macdonst committed Jul 25, 2016
1 parent 89834b7 commit e835ca3
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 21 deletions.
81 changes: 80 additions & 1 deletion docs/PAYLOAD.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Stacking](#stacking)
- [Inbox Stacking](#inbox-stacking)
- [Action Buttons](#action-buttons)
- [In Line Replies](#in-line-replies)
- [Led in Notifications](#led-in-notifications)
- [Vibration Pattern in Notifications](#vibration-pattern-in-notifications)
- [Priority in Notifications](#priority-in-notifications)
Expand Down Expand Up @@ -548,14 +549,92 @@ This will produce the following notification in your tray:

If your users clicks on the main body of the notification your app will be opened. However if they click on either of the action buttons the app will open (or start) and the specified JavaScript callback will be executed. In this case it is `app.emailGuests` and `app.snooze` respectively. If you set the `foreground` property to `true` the app will be brought to the front, if `foreground` is `false` then the callback is run without the app being brought to the foreground.

### Attributes
### In Line Replies

Android N introduces a new capability for push notifications, the in line reply text field. If you wish to get some text data from the user when the action button is called send the following type of payload:

Your notification can include action buttons. If you wish to include an icon along with the button name they must be placed in the `res/drawable` directory of your Android project. Then you can send the following JSON from GCM:

```javascript
{
"registration_ids": ["my device id"],
"data": {
"title": "AUX Scrum",
"message": "Scrum: Daily touchbase @ 10am Please be on time so we can cover everything on the agenda.",
"actions": [
{ "icon": "emailGuests", "title": "EMAIL GUESTS", "callback": "app.emailGuests", "foreground": false, "inline": true },
{ "icon": "snooze", "title": "SNOOZE", "callback": "app.snooze", "foreground": false}
]
}
}
```

Here is an example using node-gcm that sends the above JSON:

```javascript
var gcm = require('node-gcm');
// Replace these with your own values.
var apiKey = "replace with API key";
var deviceID = "my device id";
var service = new gcm.Sender(apiKey);
var message = new gcm.Message();
message.addData('title', 'AUX Scrum');
message.addData('message', 'Scrum: Daily touchbase @ 10am Please be on time so we can cover everything on the agenda.');
message.addData('actions', [
{ "icon": "emailGuests", "title": "EMAIL GUESTS", "callback": "app.emailGuests", "foreground": false, "inline": true},
{ "icon": "snooze", "title": "SNOOZE", "callback": "app.snooze", "foreground": false},
]);
service.send(message, { registrationTokens: [ deviceID ] }, function (err, response) {
if(err) console.error(err);
else console.log(response);
});
```

On Android M and earlier the action buttons will work exactly the same as before but on Android N and greater when the user clicks on the Email Guests button you will see the following:

![inline_reply](https://cloud.githubusercontent.com/assets/353180/17107608/f35c208e-525d-11e6-94de-a3590c6f500d.png)

Then your app's `on('notification')` event handler will be called without the app being brought to the foreground and the event data would be:

```
{
"title": "AUX Scrum",
"message": "Scrum: Daily touchbase @ 10am Please be on time so we can cover everything on the agenda.",
"additionalData": {
"inlineReply": "Sounds good",
"actions": [
{
"inline": true,
"callback": "app.accept",
"foreground": false,
"title": "Accept"
},
{
"icon": "snooze",
"callback": "app.reject",
"foreground": false,
"title": "Reject"
}
],
"actionCallback": "app.accept",
"coldstart": false,
"collapse_key": "do_not_collapse",
"foreground": false
}
}
```

and the text data that the user typed would be located in `data.additionalData.inlineReply`.

#### Attributes

Attribute | Type | Default | Description
--------- | ---- | ------- | -----------
`icon` | `string` | | Optional. The name of a drawable resource to use as the small-icon. The name should not include the extension.
`title` | `string` | | Required. The label to display for the action button.
`callback` | `string` | | Required. The function to be executed when the action button is pressed. The function must be accessible from the global namespace. If you provide `myCallback` then it amounts to calling `window.myCallback`. If you provide `app.myCallback` then there needs to be an object call `app`, with a function called `myCallback` accessible from the global namespace, i.e. `window.app.myCallback`.
`foreground` | `boolean` | `true` | Optional. Whether or not to bring the app to the foreground when the action button is pressed.
`inline` | `boolean` | `false` | Optional. Whether or not to provide a quick reply text field to the user when the button is clicked.

## Led in Notifications

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.support.v4.app.RemoteInput;

public class BackgroundActionButtonHandler extends BroadcastReceiver implements PushConstants {
private static String LOG_TAG = "PushPlugin_BackgroundActionButtonHandler";
Expand All @@ -26,6 +27,14 @@ public void onReceive(Context context, Intent intent) {
originalExtras.putBoolean(FOREGROUND, false);
originalExtras.putBoolean(COLDSTART, false);
originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK));

Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString();
Log.d(LOG_TAG, "response: " + inputString);
originalExtras.putString(INLINE_REPLY, inputString);
}

PushPlugin.sendExtras(originalExtras);
}
}
Expand Down
72 changes: 56 additions & 16 deletions src/android/com/adobe/phonegap/push/GCMIntentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.NotificationCompat.WearableExtender;
import android.support.v4.app.RemoteInput;
import android.text.Html;
import android.text.Spanned;
import android.util.Log;
Expand Down Expand Up @@ -363,8 +364,15 @@ public void createNotification(Context context, Bundle extras) {
mNotificationManager.notify(appName, notId, mBuilder.build());
}

private void updateIntent(Intent intent, String callback, Bundle extras, boolean foreground, int notId) {
intent.putExtra(CALLBACK, callback);
intent.putExtra(PUSH_BUNDLE, extras);
intent.putExtra(FOREGROUND, foreground);
intent.putExtra(NOT_ID, notId);
}

private void createActions(Bundle extras, NotificationCompat.Builder mBuilder, Resources resources, String packageName, int notId) {
Log.d(LOG_TAG, "create actions");
Log.d(LOG_TAG, "create actions: with in-line");
String actions = extras.getString(ACTIONS);
if (actions != null) {
try {
Expand All @@ -375,30 +383,62 @@ private void createActions(Bundle extras, NotificationCompat.Builder mBuilder, R
JSONObject action = actionsArray.getJSONObject(i);
Log.d(LOG_TAG, "adding callback = " + action.getString(CALLBACK));
boolean foreground = action.optBoolean(FOREGROUND, true);
boolean inline = action.optBoolean("inline", false);
Intent intent = null;
PendingIntent pIntent = null;
if (foreground) {
if (inline) {
Log.d(LOG_TAG, "Version: " + android.os.Build.VERSION.SDK_INT + " = " + android.os.Build.VERSION_CODES.M);
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) {
Log.d(LOG_TAG, "push activity");
intent = new Intent(this, PushHandlerActivity.class);
} else {
Log.d(LOG_TAG, "push receiver");
intent = new Intent(this, BackgroundActionButtonHandler.class);
}

updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);

if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) {
Log.d(LOG_TAG, "push activity");
pIntent = PendingIntent.getActivity(this, i, intent, PendingIntent.FLAG_ONE_SHOT);
} else {
Log.d(LOG_TAG, "push receiver");
pIntent = PendingIntent.getBroadcast(this, i, intent, PendingIntent.FLAG_ONE_SHOT);
}
} else if (foreground) {
intent = new Intent(this, PushHandlerActivity.class);
intent.putExtra(CALLBACK, action.getString(CALLBACK));
intent.putExtra(PUSH_BUNDLE, extras);
intent.putExtra(FOREGROUND, foreground);
intent.putExtra(NOT_ID, notId);
updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);
pIntent = PendingIntent.getActivity(this, i, intent, PendingIntent.FLAG_UPDATE_CURRENT);
} else {
intent = new Intent(this, BackgroundActionButtonHandler.class);
intent.putExtra(CALLBACK, action.getString(CALLBACK));
intent.putExtra(PUSH_BUNDLE, extras);
intent.putExtra(FOREGROUND, foreground);
intent.putExtra(NOT_ID, notId);
updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);
pIntent = PendingIntent.getBroadcast(this, i, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
NotificationCompat.Action wAction =
new NotificationCompat.Action.Builder(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent)
.build();
wActions.add(wAction);
mBuilder.addAction(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),

NotificationCompat.Action.Builder actionBuilder =
new NotificationCompat.Action.Builder(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent);

RemoteInput remoteInput = null;
if (inline) {
Log.d(LOG_TAG, "create remote input");
String replyLabel = "Enter your reply here";
remoteInput =
new RemoteInput.Builder(INLINE_REPLY)
.setLabel(replyLabel)
.build();
actionBuilder.addRemoteInput(remoteInput);
}

NotificationCompat.Action wAction = actionBuilder.build();
wActions.add(actionBuilder.build());

if (inline) {
mBuilder.addAction(wAction);
} else {
mBuilder.addAction(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent);
}
wAction = null;
pIntent = null;
}
Expand Down
1 change: 1 addition & 0 deletions src/android/com/adobe/phonegap/push/PushConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ public interface PushConstants {
public static final String SET_APPLICATION_ICON_BADGE_NUMBER = "setApplicationIconBadgeNumber";
public static final String CLEAR_ALL_NOTIFICATIONS = "clearAllNotifications";
public static final String VISIBILITY = "visibility";
public static final String INLINE_REPLY = "inlineReply";
}
22 changes: 18 additions & 4 deletions src/android/com/adobe/phonegap/push/PushHandlerActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.support.v4.app.RemoteInput;


public class PushHandlerActivity extends Activity implements PushConstants {
private static String LOG_TAG = "PushPlugin_PushHandlerActivity";
Expand All @@ -20,7 +22,10 @@ public class PushHandlerActivity extends Activity implements PushConstants {
@Override
public void onCreate(Bundle savedInstanceState) {
GCMIntentService gcm = new GCMIntentService();
int notId = getIntent().getExtras().getInt(NOT_ID, 0);

Intent intent = getIntent();

int notId = intent.getExtras().getInt(NOT_ID, 0);
Log.d(LOG_TAG, "not id = " + notId);
gcm.setNotification(notId, "");
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Expand All @@ -34,13 +39,13 @@ public void onCreate(Bundle savedInstanceState) {
Log.d(LOG_TAG, "bringToForeground = " + foreground);

boolean isPushPluginActive = PushPlugin.isActive();
processPushBundle(isPushPluginActive);
boolean inline = processPushBundle(isPushPluginActive, intent);

finish();

Log.d(LOG_TAG, "isPushPluginActive = " + isPushPluginActive);

if (!isPushPluginActive && foreground) {
if (!isPushPluginActive && foreground && inline) {
Log.d(LOG_TAG, "forceMainActivityReload");
forceMainActivityReload();
} else {
Expand All @@ -52,8 +57,9 @@ public void onCreate(Bundle savedInstanceState) {
* Takes the pushBundle extras from the intent,
* and sends it through to the PushPlugin for processing.
*/
private void processPushBundle(boolean isPushPluginActive) {
private boolean processPushBundle(boolean isPushPluginActive, Intent intent) {
Bundle extras = getIntent().getExtras();
Bundle remoteInput = null;

if (extras != null) {
Bundle originalExtras = extras.getBundle(PUSH_BUNDLE);
Expand All @@ -62,8 +68,16 @@ private void processPushBundle(boolean isPushPluginActive) {
originalExtras.putBoolean(COLDSTART, !isPushPluginActive);
originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK));

remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString();
Log.d(LOG_TAG, "response: " + inputString);
originalExtras.putString(INLINE_REPLY, inputString);
}

PushPlugin.sendExtras(originalExtras);
}
return remoteInput == null;
}

/**
Expand Down

0 comments on commit e835ca3

Please sign in to comment.