From e835ca31ac02a3455ece8c96938260935e2e7100 Mon Sep 17 00:00:00 2001 From: Simon MacDonald Date: Thu, 14 Jul 2016 19:39:44 -0400 Subject: [PATCH] Issue #683: Support Android N inline reply actions --- docs/PAYLOAD.md | 81 ++++++++++++++++++- .../push/BackgroundActionButtonHandler.java | 9 +++ .../adobe/phonegap/push/GCMIntentService.java | 72 +++++++++++++---- .../adobe/phonegap/push/PushConstants.java | 1 + .../phonegap/push/PushHandlerActivity.java | 22 ++++- 5 files changed, 164 insertions(+), 21 deletions(-) diff --git a/docs/PAYLOAD.md b/docs/PAYLOAD.md index f81293a05..ccf04e516 100644 --- a/docs/PAYLOAD.md +++ b/docs/PAYLOAD.md @@ -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) @@ -548,7 +549,84 @@ 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 --------- | ---- | ------- | ----------- @@ -556,6 +634,7 @@ Attribute | Type | Default | Description `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 diff --git a/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java b/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java index d10133fcc..3ccea6cb5 100644 --- a/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java +++ b/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java @@ -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"; @@ -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); } } diff --git a/src/android/com/adobe/phonegap/push/GCMIntentService.java b/src/android/com/adobe/phonegap/push/GCMIntentService.java index fa7f09604..04ff2fb84 100644 --- a/src/android/com/adobe/phonegap/push/GCMIntentService.java +++ b/src/android/com/adobe/phonegap/push/GCMIntentService.java @@ -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; @@ -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 { @@ -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; } diff --git a/src/android/com/adobe/phonegap/push/PushConstants.java b/src/android/com/adobe/phonegap/push/PushConstants.java index a0c65c3b9..710912f83 100644 --- a/src/android/com/adobe/phonegap/push/PushConstants.java +++ b/src/android/com/adobe/phonegap/push/PushConstants.java @@ -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"; } diff --git a/src/android/com/adobe/phonegap/push/PushHandlerActivity.java b/src/android/com/adobe/phonegap/push/PushHandlerActivity.java index 04daa12fe..8bc4898c6 100644 --- a/src/android/com/adobe/phonegap/push/PushHandlerActivity.java +++ b/src/android/com/adobe/phonegap/push/PushHandlerActivity.java @@ -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"; @@ -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); @@ -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 { @@ -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); @@ -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; } /**