From 17f7f39dac29aaa19753ffb9ef3689400ae7da36 Mon Sep 17 00:00:00 2001 From: Dariusz Luksza Date: Fri, 11 May 2018 23:09:32 +0200 Subject: [PATCH] Implement handling of Android actions in background There are some cases when local notification action should be handled in background eg. snoozing the reminder. In case of it launching app UI is not necessary and would be confusing for the end user. Therefore there should be a way to handle local notification action in background. For this reason new property 'runInBackground' was added to the AndroidAction class and TypeScript type. Also new broadcast receiver and service were implemented to handle properly background actions. In order to run particular action in background API consumer need to set its 'runInBackground' property to 'true', eg: ... .android.addAction(new firebase.notifications.Android.Action("snooze", "ic_snooze", "Snooze").setRunInBackground(true)) ... Then, there are two cases that API consumer needs to handle. First when app is in the foreground, standard notification and notification action code path will be executed. This mean, that: * onNotification() listener will be called (which should call displayNotification(), in order to show it to the user), * onNotificationOpen() listener will be called after the action is tapped by the user Secondly, when application is in background or it is not running new 'RNFirebaseBackgroundNotificationAction' handler will be called. To properly handle this case API consumer should create a background asynchronous handler: const handleAsyncTask = async (notificationOpen: NotifficationOpen) => { if (notificationOpen && notificationOpen.notification) { const action = notificationOpen.action; const notificationId = notificationOpen.notification.notificationId; if (action === "snooze") { console.log("Reschedule notification for later time", notificationId); } else { console.log("unsupported action", action); } // hide the notification firebase.notifications().removeDeliveredNotification(notificationId); } } Next hander should be registered to headless handler: AppRegistry.registerHeadlessTask('RNFirebaseBackgroundNotificationAction', () => handleAsyncTask); Finally AndroidManifest.xml file must be modified, to include receiver and service definition: Now when ever 'Snooze' action is pressed it will launch 'handleAsyncTask' function in the background or onNotificationOpen() when app is in foreground. And reschedule the notification for the later time. --- .../DisplayNotificationTask.java | 19 +++++-- ...eBackgroundNotificationActionReceiver.java | 50 +++++++++++++++++++ ...eBackgroundNotificationActionsService.java | 29 +++++++++++ lib/index.d.ts | 2 + lib/modules/notifications/AndroidAction.js | 19 +++++++ lib/modules/notifications/types.js | 1 + 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionReceiver.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionsService.java diff --git a/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java b/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java index cb2b069f..e21073ee 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java +++ b/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java @@ -294,9 +294,11 @@ public class DisplayNotificationTask extends AsyncTask { } private NotificationCompat.Action createAction(Bundle action, Class intentClass, Bundle notification) { + boolean runInBackground = action.containsKey("runInBackground") && action.getBoolean("runInBackground"); String actionKey = action.getString("action"); - PendingIntent actionIntent = createIntent(intentClass, notification, actionKey); - + PendingIntent actionIntent = runInBackground ? + createBroadcastIntent(notification, actionKey) : + createIntent(intentClass, notification, actionKey); int icon = getIcon(action.getString("icon")); String title = action.getString("title"); @@ -334,10 +336,21 @@ public class DisplayNotificationTask extends AsyncTask { } String notificationId = notification.getString("notificationId"); - return PendingIntent.getActivity(context, notificationId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } + private PendingIntent createBroadcastIntent(Bundle notification, String action) { + Intent intent = new Intent(context, RNFirebaseBackgroundNotificationActionReceiver.class); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + + String notificationId = notification.getString("notificationId") + action; + + intent.setAction("io.invertase.firebase.notifications.BackgroundAction"); + intent.putExtra("action", action); + intent.putExtra("notification", notification); + return PendingIntent.getBroadcast(context, notificationId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + private RemoteInput createRemoteInput(Bundle remoteInput) { String resultKey = remoteInput.getString("resultKey"); diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionReceiver.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionReceiver.java new file mode 100644 index 00000000..4ac2db87 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionReceiver.java @@ -0,0 +1,50 @@ +package io.invertase.firebase.notifications; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.ReactApplication; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; + +import io.invertase.firebase.Utils; + +public class RNFirebaseBackgroundNotificationActionReceiver extends BroadcastReceiver { + static boolean isBackgroundNotficationIntent(Intent intent) { + return intent.getExtras() != null && intent.hasExtra("action") && intent.hasExtra("notification"); + } + + static WritableMap toNotificationOpenMap(Intent intent) { + Bundle extras = intent.getExtras(); + WritableMap notificationMap = Arguments.makeNativeMap(extras.getBundle("notification")); + WritableMap notificationOpenMap = Arguments.createMap(); + notificationOpenMap.putString("action", extras.getString("action")); + notificationOpenMap.putMap("notification", notificationMap); + return notificationOpenMap; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!isBackgroundNotficationIntent(intent)) { + return; + } + + if (Utils.isAppInForeground(context)) { + WritableMap notificationOpenMap = toNotificationOpenMap(intent); + + ReactApplication reactApplication = (ReactApplication)context.getApplicationContext(); + ReactContext reactContext = reactApplication.getReactNativeHost().getReactInstanceManager().getCurrentReactContext(); + + Utils.sendEvent(reactContext, "notifications_notification_opened", notificationOpenMap); + } else { + Intent serviceIntent = new Intent(context, RNFirebaseBackgroundNotificationActionsService.class); + serviceIntent.putExtras(intent.getExtras()); + context.startService(serviceIntent); + HeadlessJsTaskService.acquireWakeLockNow(context); + } + } +} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionsService.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionsService.java new file mode 100644 index 00000000..9bfb83bb --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseBackgroundNotificationActionsService.java @@ -0,0 +1,29 @@ +package io.invertase.firebase.notifications; + +import android.content.Intent; + +import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.jstasks.HeadlessJsTaskConfig; + +import javax.annotation.Nullable; + +import static io.invertase.firebase.notifications.RNFirebaseBackgroundNotificationActionReceiver.isBackgroundNotficationIntent; +import static io.invertase.firebase.notifications.RNFirebaseBackgroundNotificationActionReceiver.toNotificationOpenMap; + +public class RNFirebaseBackgroundNotificationActionsService extends HeadlessJsTaskService { + @Override + protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) { + if (isBackgroundNotficationIntent(intent)) { + WritableMap notificationOpenMap = toNotificationOpenMap(intent); + + return new HeadlessJsTaskConfig( + "RNFirebaseBackgroundNotificationAction", + notificationOpenMap, + 60000, + true + ); + } + return null; + } +} diff --git a/lib/index.d.ts b/lib/index.d.ts index c7bf9fb2..ae41b750 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1277,6 +1277,7 @@ declare module 'react-native-firebase' { semanticAction?: SemanticAction; showUserInterface?: boolean; title: string; + runInBackground?: boolean; constructor(action: string, icon: string, title: string); @@ -1284,6 +1285,7 @@ declare module 'react-native-firebase' { setAllowGenerateReplies(allowGeneratedReplies: boolean): Action; setSemanticAction(semanticAction: SemanticAction): Action; setShowUserInterface(showUserInterface: boolean): Action; + setRunInBackground(runInBackground: boolean): Action; } class RemoteInput { diff --git a/lib/modules/notifications/AndroidAction.js b/lib/modules/notifications/AndroidAction.js index ed2a940c..c52f9c13 100644 --- a/lib/modules/notifications/AndroidAction.js +++ b/lib/modules/notifications/AndroidAction.js @@ -16,6 +16,7 @@ export default class AndroidAction { _semanticAction: SemanticActionType | void; _showUserInterface: boolean | void; _title: string; + _runInBackground: boolean | void; constructor(action: string, icon: string, title: string) { this._action = action; @@ -52,6 +53,10 @@ export default class AndroidAction { return this._title; } + get runInBackground(): ?boolean { + return this._runInBackground; + } + /** * * @param remoteInput @@ -102,6 +107,16 @@ export default class AndroidAction { return this; } + /** + * + * @param runInBackground + * @returns {AndroidAction} + */ + setRunInBackground(runInBackground: boolean): AndroidAction { + this._runInBackground = runInBackground + return this; + } + build(): NativeAndroidAction { if (!this._action) { throw new Error('AndroidAction: Missing required `action` property'); @@ -119,6 +134,7 @@ export default class AndroidAction { semanticAction: this._semanticAction, showUserInterface: this._showUserInterface, title: this._title, + runInBackground: this._runInBackground }; } } @@ -145,6 +161,9 @@ export const fromNativeAndroidAction = ( if (nativeAction.showUserInterface) { action.setShowUserInterface(nativeAction.showUserInterface); } + if (nativeAction.runInBackground) { + action.setRunInBackground(nativeAction.runInBackground); + } return action; }; diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 780eabab..c94cbd7b 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -136,6 +136,7 @@ export type NativeAndroidAction = {| semanticAction?: SemanticActionType, showUserInterface?: boolean, title: string, + runInBackground?: boolean, |}; export type NativeAndroidNotification = {|