From 62934a32190f7c11985c18f9049246013119db8c Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 10 Aug 2021 18:52:15 +0200 Subject: [PATCH 1/6] feature: test NotificationServiceExtension --- android/gradlew | 0 .../onesignal/flutter/OneSignalPlugin.java | 34 +++++ .../flutter/OneSignalSerializer.java | 4 +- example/.flutter-plugins-dependencies | 1 + example/.idea/libraries/Dart_SDK.xml | 31 +++-- example/.idea/workspace.xml | 68 ++++++---- example/android/app/build.gradle | 6 + .../android/app/src/main/AndroidManifest.xml | 35 ++++-- .../onesignalexample/MainActivity.java | 13 -- .../onesignalexample/MainActivity.kt | 18 +++ .../NotificationServiceExtension.kt | 41 ++++++ .../app/src/main/res/values/styles.xml | 12 +- example/android/build.gradle | 3 + example/android/settings_aar.gradle | 1 + .../ios/Flutter/flutter_export_environment.sh | 14 +++ example/lib/main.dart | 95 +++++++------- example/onesignal_example.iml | Bin 896 -> 825 bytes example/pubspec.yaml | 3 +- lib/onesignal_flutter.dart | 118 +++++++++++------- 19 files changed, 345 insertions(+), 152 deletions(-) mode change 100644 => 100755 android/gradlew create mode 100644 example/.flutter-plugins-dependencies delete mode 100644 example/android/app/src/main/java/com/onesignal/onesignalexample/MainActivity.java create mode 100644 example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt create mode 100644 example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt create mode 100644 example/android/settings_aar.gradle create mode 100755 example/ios/Flutter/flutter_export_environment.sh diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java index 129868fa..3a94b826 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java @@ -1,5 +1,6 @@ package com.onesignal.flutter; +import android.app.Activity; import android.content.Context; import com.onesignal.OSDeviceState; @@ -25,6 +26,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -52,6 +54,8 @@ public class OneSignalPlugin private boolean hasSetRequiresPrivacyConsent = false; private boolean waitingForUserPrivacyConsent = false; + private Activity mainActivity; + private HashMap notificationReceivedEventCache = new HashMap<>(); public static void registerWith(Registrar registrar) { @@ -63,6 +67,7 @@ public static void registerWith(Registrar registrar) { plugin.channel = new MethodChannel(registrar.messenger(), "OneSignal"); plugin.channel.setMethodCallHandler(plugin); plugin.flutterRegistrar = registrar; + plugin.mainActivity = registrar.activity(); // Create a callback for the flutterRegistrar to connect the applications onDestroy plugin.flutterRegistrar.addViewDestroyListener(new PluginRegistry.ViewDestroyListener() { @@ -128,6 +133,8 @@ else if (call.method.contentEquals("OneSignal#initInAppMessageClickedHandlerPara this.initInAppMessageClickedHandlerParams(); else if (call.method.contentEquals("OneSignal#initNotificationWillShowInForegroundHandlerParams")) this.initNotificationWillShowInForegroundHandlerParams(); + else if (call.method.contentEquals("OneSignal#initNotificationWillShowHandlerParams")) + this.initNotificationWillShowHandlerParams(call, result); else if (call.method.contentEquals("OneSignal#completeNotification")) this.completeNotification(call, result); else if (call.method.contentEquals("OneSignal#clearOneSignalNotifications")) @@ -301,6 +308,33 @@ private void initInAppMessageClickedHandlerParams() { } } + private void initNotificationWillShowHandlerParams(MethodCall call, Result result) { + this.hasSetNotificationWillShowInForegroundHandler = true; + + @SuppressWarnings("unchecked") + Map arguments = ((Map) call.arguments); + + long pluginCallbackHandle = 0; + + Object arg1 = arguments.get("notificationCallbackHandle"); + + if (arg1 instanceof Long) { + pluginCallbackHandle = (Long) arg1; + } else { + pluginCallbackHandle = Long.valueOf((Integer) arg1); + } + + + FlutterShellArgs shellArgs = null; + if (mainActivity != null) { + // Supports both Flutter Activity types: + // io.flutter.embedding.android.FlutterFragmentActivity + // io.flutter.embedding.android.FlutterActivity + // We could use `getFlutterShellArgs()` but this is only available on `FlutterActivity`. + shellArgs = FlutterShellArgs.fromIntent(mainActivity.getIntent()); + } + } + private void initNotificationWillShowInForegroundHandlerParams() { this.hasSetNotificationWillShowInForegroundHandler = true; } diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java b/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java index 24d36e2a..97269fe2 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalSerializer.java @@ -28,7 +28,7 @@ import java.util.Iterator; import java.util.List; -class OneSignalSerializer { +public class OneSignalSerializer { private static HashMap convertSubscriptionStateToMap(OSSubscriptionState state) { HashMap hash = new HashMap<>(); @@ -232,7 +232,7 @@ static HashMap convertInAppMessageClickedActionToMap(OSInAppMess return hash; } - static HashMap convertNotificationReceivedEventToMap(OSNotificationReceivedEvent notificationReceivedEvent) throws JSONException { + public static HashMap convertNotificationReceivedEventToMap(OSNotificationReceivedEvent notificationReceivedEvent) throws JSONException { return convertNotificationToMap(notificationReceivedEvent.getNotification()); } diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies new file mode 100644 index 00000000..5baeb2e3 --- /dev/null +++ b/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-10 18:48:46.766599","version":"2.2.3"} \ No newline at end of file diff --git a/example/.idea/libraries/Dart_SDK.xml b/example/.idea/libraries/Dart_SDK.xml index 5a16d312..62be7eab 100644 --- a/example/.idea/libraries/Dart_SDK.xml +++ b/example/.idea/libraries/Dart_SDK.xml @@ -1,17 +1,26 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/example/.idea/workspace.xml b/example/.idea/workspace.xml index 5b3388cc..eb69491f 100644 --- a/example/.idea/workspace.xml +++ b/example/.idea/workspace.xml @@ -1,36 +1,62 @@ - - - - - - - - - - - - + + + + + + + - - - - - + + + + + + - - + + + - + + + - + + + + + 1628559160573 + + + + + + + + + + + + \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b8b6e592..a3af5dca 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,11 +26,16 @@ if (flutterRoot == null) { } apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 28 + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + lintOptions { disable 'InvalidPackage' } @@ -62,4 +67,5 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index fba2fd08..56a3b49d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.onesignal.onesignalexample"> - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + + + + + + + diff --git a/example/android/app/src/main/java/com/onesignal/onesignalexample/MainActivity.java b/example/android/app/src/main/java/com/onesignal/onesignalexample/MainActivity.java deleted file mode 100644 index c8ac8c4a..00000000 --- a/example/android/app/src/main/java/com/onesignal/onesignalexample/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.onesignal.onesignalexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt new file mode 100644 index 00000000..95896d57 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt @@ -0,0 +1,18 @@ +package com.onesignal.onesignalexample + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +open class MainActivity: FlutterActivity() { + + companion object { + @JvmStatic + var flutterEngineInstance: FlutterEngine? = null + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + flutterEngineInstance = flutterEngine + } + +} diff --git a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt new file mode 100644 index 00000000..87f8d72c --- /dev/null +++ b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt @@ -0,0 +1,41 @@ +package com.onesignal.onesignalexample + +import android.content.Context +import android.util.Log +import org.json.JSONObject + +import com.onesignal.onesignalexample.MainActivity +import com.onesignal.flutter.OneSignalSerializer +import io.flutter.plugin.common.MethodChannel +import com.onesignal.OSNotification +import com.onesignal.OSMutableNotification +import com.onesignal.OSNotificationReceivedEvent +import com.onesignal.OneSignal.OSRemoteNotificationReceivedHandler + +class NotificationServiceExtension : OSRemoteNotificationReceivedHandler { + override fun remoteNotificationReceived( + context: Context, + notificationReceivedEvent: OSNotificationReceivedEvent + ) { + val notification = notificationReceivedEvent.notification + + // Example of modifying the notification's accent color + val mutableNotification = notification.mutableCopy() + // mutableNotification.setExtender(builder -> builder.setColor(context.getResources().getColor(R.color.colorPrimary))); + val data = notification.additionalData + Log.i("OneSignalExample", "Received Notification Data: $data") + + var receivedMap = OneSignalSerializer.convertNotificationReceivedEventToMap(notificationReceivedEvent); + + MainActivity.flutterEngineInstance?.let { + MethodChannel( + it.dartExecutor.binaryMessenger, + "com.onesignal.flutter.OneSignalPlugin" + ).invokeMethod("OneSignal#handleNotificationWillShowInForeground", receivedMap); + } + + // If complete isn't call within a time period of 25 seconds, OneSignal internal logic will show the original notification + // To omit displaying a notification, pass `null` to complete() + notificationReceivedEvent.complete(null); + } +} \ No newline at end of file diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index 00fa4417..d74aa35c 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -1,8 +1,18 @@ - + + diff --git a/example/android/build.gradle b/example/android/build.gradle index e0d7ae2c..cca94918 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,4 +1,6 @@ buildscript { + ext.kotlin_version = '1.4.21' + repositories { google() jcenter() @@ -6,6 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 00000000..db748bff --- /dev/null +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/efraespada/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "SYMROOT=${SOURCE_ROOT}/../build/ios" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=false" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.packages" diff --git a/example/lib/main.dart b/example/lib/main.dart index 62c50cb8..09735396 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,13 @@ import 'dart:async'; //import OneSignal import 'package:onesignal_flutter/onesignal_flutter.dart'; +Future _backgroundHandler(OSNotificationReceivedEvent event) async { + print('NEW FOREGROUND HANDLER CALLED WITH: $event'); + + /// Display Notification, send null to not display + event.complete(event.notification); +} + void main() => runApp(new MyApp()); class MyApp extends StatefulWidget { @@ -37,28 +44,18 @@ class _MyAppState extends State { OneSignal.shared .setNotificationOpenedHandler((OSNotificationOpenedResult result) { - print('NOTIFICATION OPENED HANDLER CALLED WITH: ${result}'); - this.setState(() { - _debugLabelString = - "Opened notification: \n${result.notification.jsonRepresentation().replaceAll("\\n", "\n")}"; + print('NOTIFICATION OPENED HANDLER CALLED WITH: ${result}'); + this.setState(() { + _debugLabelString = + "Opened notification: \n${result.notification.jsonRepresentation().replaceAll("\\n", "\n")}"; }); }); - OneSignal.shared - .setNotificationWillShowInForegroundHandler((OSNotificationReceivedEvent event) { - print('FOREGROUND HANDLER CALLED WITH: ${event}'); - /// Display Notification, send null to not display - event.complete(null); - - this.setState(() { - _debugLabelString = - "Notification received in foreground notification: \n${event.notification.jsonRepresentation().replaceAll("\\n", "\n")}"; - }); - }); + OneSignal.shared.setNotificationWillShowHandler(_backgroundHandler); OneSignal.shared .setInAppMessageClickedHandler((OSInAppMessageAction action) { - this.setState(() { + this.setState(() { _debugLabelString = "In App Message Clicked: \n${action.jsonRepresentation().replaceAll("\\n", "\n")}"; }); @@ -78,14 +75,13 @@ class _MyAppState extends State { print("EMAIL SUBSCRIPTION STATE CHANGED ${changes.jsonRepresentation()}"); }); - OneSignal.shared.setSMSSubscriptionObserver( - (OSSMSSubscriptionStateChanges changes) { + OneSignal.shared + .setSMSSubscriptionObserver((OSSMSSubscriptionStateChanges changes) { print("SMS SUBSCRIPTION STATE CHANGED ${changes.jsonRepresentation()}"); }); // NOTE: Replace with your own app ID from https://www.onesignal.com - await OneSignal.shared - .setAppId("380dc082-5231-4cc2-ab51-a03da5a0e4c2"); + await OneSignal.shared.setAppId("380dc082-5231-4cc2-ab51-a03da5a0e4c2"); bool requiresConsent = await OneSignal.shared.requiresUserPrivacyConsent(); @@ -101,7 +97,8 @@ class _MyAppState extends State { // Some examples of how to use Outcome Events public methods with OneSignal SDK oneSignalOutcomeEventsExamples(); - bool userProvidedPrivacyConsent = await OneSignal.shared.userProvidedPrivacyConsent(); + bool userProvidedPrivacyConsent = + await OneSignal.shared.userProvidedPrivacyConsent(); print("USER PROVIDED PRIVACY CONSENT: $userProvidedPrivacyConsent"); } @@ -148,7 +145,8 @@ class _MyAppState extends State { OneSignal.shared.getDeviceState().then((deviceState) { print("DeviceState: ${deviceState?.jsonRepresentation()}"); this.setState(() { - _debugLabelString = deviceState?.jsonRepresentation() ?? "Device state null"; + _debugLabelString = + deviceState?.jsonRepresentation() ?? "Device state null"; }); }); } @@ -175,7 +173,7 @@ class _MyAppState extends State { }); } - void _handleSetSMSNumber() { + void _handleSetSMSNumber() { if (_smsNumber == null) return; print("Setting SMS Number"); @@ -233,29 +231,28 @@ class _MyAppState extends State { if (_externalUserId == null) return; OneSignal.shared.setExternalUserId(_externalUserId!).then((results) { - if (results == null) return; + if (results == null) return; - this.setState(() { - _debugLabelString = "External user id set: $results"; - }); + this.setState(() { + _debugLabelString = "External user id set: $results"; + }); }); } void _handleRemoveExternalUserId() { OneSignal.shared.removeExternalUserId().then((results) { - if (results == null) return; + if (results == null) return; - this.setState(() { - _debugLabelString = "External user id removed: $results"; - }); + this.setState(() { + _debugLabelString = "External user id removed: $results"; + }); }); } void _handleSendNotification() async { var deviceState = await OneSignal.shared.getDeviceState(); - if (deviceState == null || deviceState.userId == null) - return; + if (deviceState == null || deviceState.userId == null) return; var playerId = deviceState.userId!; @@ -283,8 +280,7 @@ class _MyAppState extends State { void _handleSendSilentNotification() async { var deviceState = await OneSignal.shared.getDeviceState(); - if (deviceState == null || deviceState.userId == null) - return; + if (deviceState == null || deviceState.userId == null) return; var playerId = deviceState.userId!; @@ -317,7 +313,8 @@ class _MyAppState extends State { OneSignal.shared.removeTriggerForKey("trigger_2"); // Get the value for a trigger by its key - Object? triggerValue = await OneSignal.shared.getTriggerValueForKey("trigger_3"); + Object? triggerValue = + await OneSignal.shared.getTriggerValueForKey("trigger_3"); print("'trigger_3' key trigger value: ${triggerValue?.toString()}"); // Create a list and bulk remove triggers based on keys supplied @@ -352,8 +349,8 @@ class _MyAppState extends State { } Future outcomeAwaitExample() async { - var outcomeEvent = await OneSignal.shared.sendOutcome("await_normal_1"); - print(outcomeEvent.jsonRepresentation()); + var outcomeEvent = await OneSignal.shared.sendOutcome("await_normal_1"); + print(outcomeEvent.jsonRepresentation()); } @override @@ -382,10 +379,8 @@ class _MyAppState extends State { _handlePromptForPushPermission, !_enableConsentButton) ]), new TableRow(children: [ - new OneSignalButton( - "Print Device State", - _handleGetDeviceState, - !_enableConsentButton) + new OneSignalButton("Print Device State", + _handleGetDeviceState, !_enableConsentButton) ]), new TableRow(children: [ new TextField( @@ -436,12 +431,12 @@ class _MyAppState extends State { ) ]), new TableRow(children: [ - new OneSignalButton( - "Set SMS Number", _handleSetSMSNumber, !_enableConsentButton) + new OneSignalButton("Set SMS Number", _handleSetSMSNumber, + !_enableConsentButton) ]), new TableRow(children: [ - new OneSignalButton("Logout SMS Number", _handleLogoutSMSNumber, - !_enableConsentButton) + new OneSignalButton("Logout SMS Number", + _handleLogoutSMSNumber, !_enableConsentButton) ]), new TableRow(children: [ new OneSignalButton("Provide GDPR Consent", _handleConsent, @@ -484,12 +479,12 @@ class _MyAppState extends State { ) ]), new TableRow(children: [ - new OneSignalButton( - "Set External User ID", _handleSetExternalUserId, !_enableConsentButton) + new OneSignalButton("Set External User ID", + _handleSetExternalUserId, !_enableConsentButton) ]), new TableRow(children: [ - new OneSignalButton( - "Remove External User ID", _handleRemoveExternalUserId, !_enableConsentButton) + new OneSignalButton("Remove External User ID", + _handleRemoveExternalUserId, !_enableConsentButton) ]), new TableRow(children: [ new Container( diff --git a/example/onesignal_example.iml b/example/onesignal_example.iml index e5c837191e06fa89372f5752564eab87bff82401..0fa0165e61d41aa875d7d543f9676f3f91c6a633 100644 GIT binary patch literal 825 ScmZQz7zLvtFd70RhX4QrIRF6w literal 896 zcmbW0OK-w35QOji3d`aOX{8>jDghw{q&{d7k6w{U7V6gVMt)HE_uT}yASwjn?48f+ znQ`3R6pF3EScy8}x9uCwKxddsy-N6OI%wVVZl@6!n0p1xZA(b_Q}4YuAHJotmp getDeviceState() async { - var json = - await _channel.invokeMethod("OneSignal#getDeviceState"); + var json = await _channel.invokeMethod("OneSignal#getDeviceState"); - if ((json.cast()).isEmpty) - return null; + if ((json.cast()).isEmpty) return null; return OSDeviceState(json.cast()); } @@ -263,8 +278,8 @@ class OneSignal { /// Allows you to manually cancel a single OneSignal notification based on its Android notification integer ID void removeNotification(int notificationId) { - _channel.invokeMethod("OneSignal#removeNotification", - {'notificationId': notificationId}); + _channel.invokeMethod( + "OneSignal#removeNotification", {'notificationId': notificationId}); } /// Allows you to prompt the user for permission to use location services @@ -283,7 +298,8 @@ class OneSignal { /// Identity Verification. The email auth hash is a hash of your app's API key and the /// user ID. We recommend you generate this token from your backend server, do NOT /// store your API key in your app as this is highly insecure. - Future setEmail({required String email, String? emailAuthHashToken}) async { + Future setEmail( + {required String email, String? emailAuthHashToken}) async { return await _channel.invokeMethod("OneSignal#setEmail", {'email': email, 'emailAuthHashToken': emailAuthHashToken}); } @@ -299,9 +315,11 @@ class OneSignal { /// Identity Verification. The SMS auth hash is a hash of your app's API key and the /// user ID. We recommend you generate this token from your backend server, do NOT /// store your API key in your app as this is highly insecure. - Future> setSMSNumber({required String smsNumber, String? smsAuthHashToken}) async { - Map results = - await _channel.invokeMethod("OneSignal#setSMSNumber", {'smsNumber': smsNumber, 'smsAuthHashToken': smsAuthHashToken}); + Future> setSMSNumber( + {required String smsNumber, String? smsAuthHashToken}) async { + Map results = await _channel.invokeMethod( + "OneSignal#setSMSNumber", + {'smsNumber': smsNumber, 'smsAuthHashToken': smsAuthHashToken}); return results.cast(); } @@ -316,9 +334,11 @@ class OneSignal { /// OneSignal allows you to set a custom ID for your users. This makes it so that /// if your app has its own user ID's, you can use your own custom user ID's with /// our API instead of having to save their OneSignal user ID's. - Future> setExternalUserId(String externalId, [String? authHashToken]) async { - Map results = - await (_channel.invokeMethod("OneSignal#setExternalUserId", {'externalUserId' : externalId, 'authHashToken' : authHashToken})); + Future> setExternalUserId(String externalId, + [String? authHashToken]) async { + Map results = await (_channel.invokeMethod( + "OneSignal#setExternalUserId", + {'externalUserId': externalId, 'authHashToken': authHashToken})); return results.cast(); } @@ -330,52 +350,58 @@ class OneSignal { } Future> setLanguage(String language) async { - Map results = - await (_channel.invokeMethod("OneSignal#setLanguage", {'language' : language})); + Map results = await (_channel + .invokeMethod("OneSignal#setLanguage", {'language': language})); return results.cast(); } /// Adds a single key, value trigger, which will trigger an in app message /// if one exists matching the specific trigger added Future addTrigger(String key, Object value) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#addTrigger", {key : value}); + return await _inAppMessagesChannel + .invokeMethod("OneSignal#addTrigger", {key: value}); } /// Adds one or more key, value triggers, which will trigger in app messages /// (one at a time) if any exist matching the specific triggers added Future addTriggers(Map triggers) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#addTriggers", triggers); + return await _inAppMessagesChannel.invokeMethod( + "OneSignal#addTriggers", triggers); } /// Remove a single key, value trigger to prevent an in app message from /// showing with that trigger Future removeTriggerForKey(String key) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#removeTriggerForKey", key); + return await _inAppMessagesChannel.invokeMethod( + "OneSignal#removeTriggerForKey", key); } /// Remove one or more key, value triggers to prevent any in app messages /// from showing with those triggers Future removeTriggersForKeys(List keys) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#removeTriggersForKeys", keys); + return await _inAppMessagesChannel.invokeMethod( + "OneSignal#removeTriggersForKeys", keys); } /// Get the trigger value associated with the key provided Future getTriggerValueForKey(String key) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#getTriggerValueForKey", key); + return await _inAppMessagesChannel.invokeMethod( + "OneSignal#getTriggerValueForKey", key); } /// Toggles the showing of all in app messages Future pauseInAppMessages(bool pause) async { - return await _inAppMessagesChannel.invokeMethod("OneSignal#pauseInAppMessages", pause); + return await _inAppMessagesChannel.invokeMethod( + "OneSignal#pauseInAppMessages", pause); } /// Send a normal outcome event for the current session and notifications with the attribution window /// Counted each time sent successfully, failed ones will be cached and reattempted in future Future sendOutcome(String name) async { - var json = await _outcomesChannel.invokeMethod("OneSignal#sendOutcome", name); + var json = + await _outcomesChannel.invokeMethod("OneSignal#sendOutcome", name); - if (json == null) - return new OSOutcomeEvent(); + if (json == null) return new OSOutcomeEvent(); return new OSOutcomeEvent.fromMap(json.cast()); } @@ -383,10 +409,10 @@ class OneSignal { /// Send a unique outcome event for the current session and notifications with the attribution window /// Counted once per notification when sent successfully, failed ones will be cached and reattempted in future Future sendUniqueOutcome(String name) async { - var json = await _outcomesChannel.invokeMethod("OneSignal#sendUniqueOutcome", name); + var json = await _outcomesChannel.invokeMethod( + "OneSignal#sendUniqueOutcome", name); - if (json == null) - return new OSOutcomeEvent(); + if (json == null) return new OSOutcomeEvent(); return new OSOutcomeEvent.fromMap(json.cast()); } @@ -394,10 +420,11 @@ class OneSignal { /// Send an outcome event with a value for the current session and notifications with the attribution window /// Counted each time sent successfully, failed ones will be cached and reattempted in future Future sendOutcomeWithValue(String name, double value) async { - var json = await _outcomesChannel.invokeMethod("OneSignal#sendOutcomeWithValue", {"outcome_name" : name, "outcome_value" : value}); + var json = await _outcomesChannel.invokeMethod( + "OneSignal#sendOutcomeWithValue", + {"outcome_name": name, "outcome_value": value}); - if (json == null) - return new OSOutcomeEvent(); + if (json == null) return new OSOutcomeEvent(); return new OSOutcomeEvent.fromMap(json.cast()); } @@ -415,20 +442,21 @@ class OneSignal { } else if (call.method == 'OneSignal#permissionChanged' && this._onPermissionChangedHandler != null) { this._onPermissionChangedHandler!( - OSPermissionStateChanges(call.arguments.cast())); + OSPermissionStateChanges(call.arguments.cast())); } else if (call.method == 'OneSignal#emailSubscriptionChanged' && this._onEmailSubscriptionChangedHandler != null) { - this._onEmailSubscriptionChangedHandler!( - OSEmailSubscriptionStateChanges(call.arguments.cast())); + this._onEmailSubscriptionChangedHandler!(OSEmailSubscriptionStateChanges( + call.arguments.cast())); } else if (call.method == 'OneSignal#smsSubscriptionChanged' && this._onSMSSubscriptionChangedHandler != null) { - this._onSMSSubscriptionChangedHandler!( - OSSMSSubscriptionStateChanges(call.arguments.cast())); + this._onSMSSubscriptionChangedHandler!(OSSMSSubscriptionStateChanges( + call.arguments.cast())); } else if (call.method == 'OneSignal#handleClickedInAppMessage' && this._onInAppMessageClickedHandler != null) { this._onInAppMessageClickedHandler!( OSInAppMessageAction(call.arguments.cast())); - } else if (call.method == 'OneSignal#handleNotificationWillShowInForeground' && + } else if (call.method == + 'OneSignal#handleNotificationWillShowInForeground' && this._onNotificationWillShowInForegroundHandler != null) { this._onNotificationWillShowInForegroundHandler!( OSNotificationReceivedEvent(call.arguments.cast())); From be228522b4a2a9383a38ede2fca71a1accd56dc6 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 13 Aug 2021 15:35:50 +0200 Subject: [PATCH 2/6] feature: [ANDROID] handling notifications from dart code when the app is swiped away --- android/build.gradle | 4 + .../onesignal/flutter/BackgroundExecutor.java | 304 ++++++++++++++++++ .../com/onesignal/flutter/ContextHolder.java | 15 + .../flutter/IsolateStatusHandler.java | 5 + .../onesignal/flutter/OneSignalPlugin.java | 34 +- .../XNotificationServiceExtension.java | 56 ++++ example/.flutter-plugins-dependencies | 2 +- example/android/app/build.gradle | 5 + .../onesignalexample/MainActivity.kt | 10 - .../NotificationServiceExtension.kt | 40 +-- lib/onesignal_flutter.dart | 58 +++- 11 files changed, 479 insertions(+), 54 deletions(-) create mode 100644 android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java create mode 100644 android/src/main/java/com/onesignal/flutter/ContextHolder.java create mode 100644 android/src/main/java/com/onesignal/flutter/IsolateStatusHandler.java create mode 100644 android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java diff --git a/android/build.gradle b/android/build.gradle index 9375113e..794b0725 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,6 +31,10 @@ android { lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java b/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java new file mode 100644 index 00000000..9a652401 --- /dev/null +++ b/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java @@ -0,0 +1,304 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package com.onesignal.flutter; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartCallback; +import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.FlutterCallbackInformation; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An background execution abstraction which handles initializing a background isolate running a + * callback dispatcher, used to invoke Dart callbacks while backgrounded. + */ +public class BackgroundExecutor implements MethodCallHandler { + private static final String TAG = "BackgroundExecutor"; + private static final String CALLBACK_HANDLE_KEY = "callback_handle"; + private static final String USER_CALLBACK_HANDLE_KEY = "user_callback_handle"; + + private static io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback + pluginRegistrantCallback; + private final AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); + /** + * The {@link MethodChannel} that connects the Android side of this plugin with the background + * Dart isolate that was created by this plugin. + */ + private MethodChannel backgroundChannel; + + private FlutterEngine backgroundFlutterEngine; + + /** + * Sets the {@code io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback} used to + * register plugins with the newly spawned isolate. + * + *

Note: this is only necessary for applications using the V1 engine embedding API as plugins + * are automatically registered via reflection in the V2 engine embedding API. If not set, + * background message callbacks will not be able to utilize functionality from other plugins. + */ + public static void setPluginRegistrant( + io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback callback) { + pluginRegistrantCallback = callback; + } + + /** + * Sets the Dart callback handle for the Dart method that is responsible for initializing the + * background Dart isolate, preparing it to receive Dart callback tasks requests. + */ + public static void setCallbackDispatcher(long callbackHandle) { + Context context = ContextHolder.getApplicationContext(); + SharedPreferences prefs = + context.getSharedPreferences("one_signal_key", 0); + prefs.edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply(); + } + + @Override + public void onMethodCall(MethodCall call, @NonNull Result result) { + String method = call.method; + try { + if (method.equals("OneSignal#backgroundHandlerInitialized")) { + // This message is sent by the background method channel as soon as the background isolate + // is running. From this point forward, the Android side of this plugin can send + // callback handles through the background method channel, and the Dart side will execute + // the Dart methods corresponding to those callback handles. + Log.i("OneSignal", "Background channel ready"); + + result.success(true); + } else { + result.notImplemented(); + } + } catch (Exception e) { + result.error("error", "OneSignal error: " + e.getMessage(), null); + } + } + + /** + * Returns true when the background isolate has started and is ready to handle background + * messages. + */ + public boolean isNotRunning() { + return !isCallbackDispatcherReady.get(); + } + + private void onInitialized() { + isCallbackDispatcherReady.set(true); + } + + /** + * Starts running a background Dart isolate within a new {@link FlutterEngine} using a previously + * used entrypoint. + * + *

The isolate is configured as follows: + * + *

    + *
  • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. + *
  • Entrypoint: The Dart method used the last time this plugin was initialized in the + * foreground. + *
  • Run args: none. + *
+ * + *

Preconditions: + * + *

    + *
  • The given callback must correspond to a registered Dart callback. If the handle does not + * resolve to a Dart callback then this method does nothing. + *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link + * PluginRegistrantException} will be thrown. + *
+ */ + public void startBackgroundIsolate(IsolateStatusHandler isolate) { + Log.i("OneSignal", "Starting background isolate."); + if (isNotRunning()) { + long callbackHandle = getPluginCallbackHandle(); + if (callbackHandle != 0) { + startBackgroundIsolate(callbackHandle, null, isolate); + } + } else { + Log.i(TAG, "Background isolate already started. Skipping.."); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { isolate.done(); }; + mainHandler.post(myRunnable); + } + } + + /** + * Starts running a background Dart isolate within a new {@link FlutterEngine}. + * + *

The isolate is configured as follows: + * + *

    + *
  • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. + *
  • Entrypoint: The Dart method represented by {@code callbackHandle}. + *
  • Run args: none. + *
+ * + *

Preconditions: + * + *

    + *
  • The given {@code callbackHandle} must correspond to a registered Dart callback. If the + * handle does not resolve to a Dart callback then this method does nothing. + *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link + * PluginRegistrantException} will be thrown. + *
+ */ + public void startBackgroundIsolate(long callbackHandle, FlutterShellArgs shellArgs, IsolateStatusHandler isolate) { + if (ContextHolder.getApplicationContext() == null) { + Log.i(TAG, "ApplicationContext null when starting isolation"); + return; + } + if (backgroundFlutterEngine != null) { + Log.i(TAG, "Background isolate already started."); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { isolate.done(); }; + mainHandler.post(myRunnable); + return; + } + if (!isNotRunning()) { + return; + } + + onInitialized(); + + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = + () -> { + io.flutter.view.FlutterMain.startInitialization(ContextHolder.getApplicationContext()); + io.flutter.view.FlutterMain.ensureInitializationCompleteAsync( + ContextHolder.getApplicationContext(), + null, + mainHandler, + () -> { + String appBundlePath = io.flutter.view.FlutterMain.findAppBundlePath(); + AssetManager assets = ContextHolder.getApplicationContext().getAssets(); + if (shellArgs != null) { + Log.i( + TAG, + "Creating background FlutterEngine instance, with args: " + + Arrays.toString(shellArgs.toArray())); + backgroundFlutterEngine = + new FlutterEngine( + ContextHolder.getApplicationContext(), shellArgs.toArray()); + } else { + Log.i(TAG, "Creating background FlutterEngine instance."); + backgroundFlutterEngine = + new FlutterEngine(ContextHolder.getApplicationContext()); + } + // We need to create an instance of `FlutterEngine` before looking up the + // callback. If we don't, the callback cache won't be initialized and the + // lookup will fail. + FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); + DartExecutor executor = backgroundFlutterEngine.getDartExecutor(); + initializeMethodChannel(executor); + DartCallback dartCallback = new DartCallback(assets, appBundlePath, flutterCallback); + + executor.executeDartCallback(dartCallback); + + // The pluginRegistrantCallback should only be set in the V1 embedding as + // plugin registration is done via reflection in the V2 embedding. + if (pluginRegistrantCallback != null) { + pluginRegistrantCallback.registerWith( + new ShimPluginRegistry(backgroundFlutterEngine)); + } + + isolate.done(); + }); + }; + mainHandler.post(myRunnable); +} + + boolean isDartBackgroundHandlerRegistered() { + return getPluginCallbackHandle() != 0; + } + + /** + * Executes the desired Dart callback in a background Dart isolate. + * + *

The given {@code intent} should contain a {@code long} extra called "callbackHandle", which + * corresponds to a callback registered with the Dart VM. + */ + public void executeDartCallbackInBackgroundIsolate(final HashMap receivedMap) { + if (backgroundFlutterEngine == null) { + Log.i( + TAG, + "A background message could not be handled in Dart as no onBackgroundMessage handler has been registered."); + return; + } + if (receivedMap != null) { + Log.i(TAG, "Invoking OneSignal#onBackgroundNotification"); + backgroundChannel.invokeMethod( + "OneSignal#onBackgroundNotification", + new HashMap() { + { + put("notificationCallbackHandle", getUserCallbackHandle()); + put("message", receivedMap); + } + }); + } else { + Log.e(TAG, "Notification not found."); + } + } + + /** + * Get the users registered Dart callback handle for background messaging. Returns 0 if not set. + */ + private long getUserCallbackHandle() { + if (ContextHolder.getApplicationContext() == null) { + return 0; + } + SharedPreferences prefs = + ContextHolder.getApplicationContext() + .getSharedPreferences("one_signal_key", 0); + return prefs.getLong(USER_CALLBACK_HANDLE_KEY, 0); + } + + /** + * Sets the Dart callback handle for the users Dart handler that is responsible for handling + * messaging events in the background. + */ + public static void setUserCallbackHandle(long callbackHandle) { + Context context = ContextHolder.getApplicationContext(); + SharedPreferences prefs = + context.getSharedPreferences("one_signal_key", 0); + prefs.edit().putLong(USER_CALLBACK_HANDLE_KEY, callbackHandle).apply(); + } + + /** Get the registered Dart callback handle for the messaging plugin. Returns 0 if not set. */ + private long getPluginCallbackHandle() { + if (ContextHolder.getApplicationContext() == null) { + return 0; + } + SharedPreferences prefs = + ContextHolder.getApplicationContext() + .getSharedPreferences("one_signal_key", 0); + return prefs.getLong(CALLBACK_HANDLE_KEY, 0); + } + + // This channel is responsible for sending requests from Android to Dart to execute Dart + // callbacks in the background isolate. + private void initializeMethodChannel(BinaryMessenger isolate) { + backgroundChannel = + new MethodChannel(isolate, "OneSignalBackground"); + backgroundChannel.setMethodCallHandler(this); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/onesignal/flutter/ContextHolder.java b/android/src/main/java/com/onesignal/flutter/ContextHolder.java new file mode 100644 index 00000000..58c434fc --- /dev/null +++ b/android/src/main/java/com/onesignal/flutter/ContextHolder.java @@ -0,0 +1,15 @@ +package com.onesignal.flutter; + +import android.content.Context; + +public class ContextHolder { + private static Context applicationContext; + + public static Context getApplicationContext() { + return applicationContext; + } + + public static void setApplicationContext(Context applicationContext) { + ContextHolder.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/onesignal/flutter/IsolateStatusHandler.java b/android/src/main/java/com/onesignal/flutter/IsolateStatusHandler.java new file mode 100644 index 00000000..a883dd65 --- /dev/null +++ b/android/src/main/java/com/onesignal/flutter/IsolateStatusHandler.java @@ -0,0 +1,5 @@ +package com.onesignal.flutter; + +public interface IsolateStatusHandler { + public void done(); +} \ No newline at end of file diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java index 3a94b826..d78ddb85 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.content.Context; +import android.util.Log; import com.onesignal.OSDeviceState; import com.onesignal.OSEmailSubscriptionObserver; @@ -69,6 +70,10 @@ public static void registerWith(Registrar registrar) { plugin.flutterRegistrar = registrar; plugin.mainActivity = registrar.activity(); + if (ContextHolder.getApplicationContext() == null) { + ContextHolder.setApplicationContext(plugin.mainActivity.getApplicationContext()); + } + // Create a callback for the flutterRegistrar to connect the applications onDestroy plugin.flutterRegistrar.addViewDestroyListener(new PluginRegistry.ViewDestroyListener() { @Override @@ -315,8 +320,10 @@ private void initNotificationWillShowHandlerParams(MethodCall call, Result resul Map arguments = ((Map) call.arguments); long pluginCallbackHandle = 0; + long notificationCallbackHandle = 0; - Object arg1 = arguments.get("notificationCallbackHandle"); + Object arg1 = arguments.get("pluginCallbackHandle"); + Object arg2 = arguments.get("notificationCallbackHandle"); if (arg1 instanceof Long) { pluginCallbackHandle = (Long) arg1; @@ -324,6 +331,11 @@ private void initNotificationWillShowHandlerParams(MethodCall call, Result resul pluginCallbackHandle = Long.valueOf((Integer) arg1); } + if (arg2 instanceof Long) { + notificationCallbackHandle = (Long) arg2; + } else { + notificationCallbackHandle = Long.valueOf((Integer) arg2); + } FlutterShellArgs shellArgs = null; if (mainActivity != null) { @@ -333,6 +345,22 @@ private void initNotificationWillShowHandlerParams(MethodCall call, Result resul // We could use `getFlutterShellArgs()` but this is only available on `FlutterActivity`. shellArgs = FlutterShellArgs.fromIntent(mainActivity.getIntent()); } + + if (XNotificationServiceExtension.be != null) { + Log.i("OneSignal", "Attempted to start a duplicate background isolate. Returning..."); + return; + } + + XNotificationServiceExtension.be = new BackgroundExecutor(); + XNotificationServiceExtension.be.setCallbackDispatcher(pluginCallbackHandle); + XNotificationServiceExtension.be.setUserCallbackHandle(notificationCallbackHandle); + Log.i("OneSignal", "Starting background isolate. Returning..."); + XNotificationServiceExtension.be.startBackgroundIsolate(pluginCallbackHandle, shellArgs, new IsolateStatusHandler() { + @Override + public void done() { + // nothing to do here + } + }); } private void initNotificationWillShowInForegroundHandlerParams() { @@ -357,6 +385,10 @@ private void completeNotification(MethodCall call, final Result reply) { boolean shouldDisplay = call.argument("shouldDisplay"); OSNotificationReceivedEvent notificationReceivedEvent = notificationReceivedEventCache.get(notificationId); + if (notificationReceivedEvent == null && XNotificationServiceExtension.notificationReceivedEventCache != null) { + notificationReceivedEvent = XNotificationServiceExtension.notificationReceivedEventCache.get(notificationId); + } + if (notificationReceivedEvent == null) { OneSignal.onesignalLog(OneSignal.LOG_LEVEL.ERROR, "Could not find notification completion block with id: " + notificationId); return; diff --git a/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java b/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java new file mode 100644 index 00000000..16f37695 --- /dev/null +++ b/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java @@ -0,0 +1,56 @@ +package com.onesignal.flutter; + +import android.os.Looper; +import android.os.Handler; +import android.content.Context; +import android.util.Log; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +import com.onesignal.flutter.OneSignalSerializer; +import com.onesignal.flutter.BackgroundExecutor; +import com.onesignal.flutter.IsolateStatusHandler; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.embedding.engine.FlutterEngine; +import com.onesignal.OSNotification; +import com.onesignal.OSMutableNotification; +import com.onesignal.OSNotificationReceivedEvent; +import com.onesignal.OneSignal.OSRemoteNotificationReceivedHandler; + +public class XNotificationServiceExtension implements OSRemoteNotificationReceivedHandler { + + public static BackgroundExecutor be; + public static HashMap notificationReceivedEventCache = new HashMap<>(); + + @Override + public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent notificationReceivedEvent) { + OSNotification notification = notificationReceivedEvent.getNotification(); + XNotificationServiceExtension.notificationReceivedEventCache.put(notification.getNotificationId(), notificationReceivedEvent); + JSONObject data = notification.getAdditionalData(); + Log.i("OneSignal", "Received Notification Data: " + data.toString()); + // notificationReceivedEvent.complete(null); + + if (ContextHolder.getApplicationContext() == null) { + ContextHolder.setApplicationContext(context.getApplicationContext()); + } + if (XNotificationServiceExtension.be == null) { + XNotificationServiceExtension.be = new BackgroundExecutor(); + } + Log.i("OneSignal", "Checking isolated BackgroundExecutor."); + try { + HashMap receivedMap = OneSignalSerializer.convertNotificationReceivedEventToMap(notificationReceivedEvent); + XNotificationServiceExtension.be.startBackgroundIsolate(new IsolateStatusHandler() { + @Override + public void done() { + if (XNotificationServiceExtension.be != null) { + XNotificationServiceExtension.be.executeDartCallbackInBackgroundIsolate(receivedMap); + } + } + }); + } catch (Exception e) { + Log.i("OneSignal", "Exception on XNotificationServiceExtension", e); + } + } +} \ No newline at end of file diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 5baeb2e3..3db6583e 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-10 18:48:46.766599","version":"2.2.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-12 01:52:39.082971","version":"2.2.3"} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index a3af5dca..77890719 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -57,6 +57,11 @@ android { signingConfig signingConfigs.debug } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } flutter { diff --git a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt index 95896d57..85a273d6 100644 --- a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/MainActivity.kt @@ -5,14 +5,4 @@ import io.flutter.embedding.engine.FlutterEngine open class MainActivity: FlutterActivity() { - companion object { - @JvmStatic - var flutterEngineInstance: FlutterEngine? = null - } - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - flutterEngineInstance = flutterEngine - } - } diff --git a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt index 87f8d72c..9c173162 100644 --- a/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt +++ b/example/android/app/src/main/kotlin/com/onesignal/onesignalexample/NotificationServiceExtension.kt @@ -1,41 +1,5 @@ package com.onesignal.onesignalexample -import android.content.Context -import android.util.Log -import org.json.JSONObject +import com.onesignal.flutter.XNotificationServiceExtension -import com.onesignal.onesignalexample.MainActivity -import com.onesignal.flutter.OneSignalSerializer -import io.flutter.plugin.common.MethodChannel -import com.onesignal.OSNotification -import com.onesignal.OSMutableNotification -import com.onesignal.OSNotificationReceivedEvent -import com.onesignal.OneSignal.OSRemoteNotificationReceivedHandler - -class NotificationServiceExtension : OSRemoteNotificationReceivedHandler { - override fun remoteNotificationReceived( - context: Context, - notificationReceivedEvent: OSNotificationReceivedEvent - ) { - val notification = notificationReceivedEvent.notification - - // Example of modifying the notification's accent color - val mutableNotification = notification.mutableCopy() - // mutableNotification.setExtender(builder -> builder.setColor(context.getResources().getColor(R.color.colorPrimary))); - val data = notification.additionalData - Log.i("OneSignalExample", "Received Notification Data: $data") - - var receivedMap = OneSignalSerializer.convertNotificationReceivedEventToMap(notificationReceivedEvent); - - MainActivity.flutterEngineInstance?.let { - MethodChannel( - it.dartExecutor.binaryMessenger, - "com.onesignal.flutter.OneSignalPlugin" - ).invokeMethod("OneSignal#handleNotificationWillShowInForeground", receivedMap); - } - - // If complete isn't call within a time period of 25 seconds, OneSignal internal logic will show the original notification - // To omit displaying a notification, pass `null` to complete() - notificationReceivedEvent.complete(null); - } -} \ No newline at end of file +class NotificationServiceExtension : XNotificationServiceExtension() \ No newline at end of file diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index 51c54a03..ed741e13 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/src/permission.dart'; import 'package:onesignal_flutter/src/subscription.dart'; @@ -31,6 +33,47 @@ typedef void InAppMessageClickedHandler(OSInAppMessageAction action); typedef void NotificationWillShowInForegroundHandler( OSNotificationReceivedEvent event); +void _callbackDispatcher() { + // Initialize state necessary for MethodChannels. + WidgetsFlutterBinding.ensureInitialized(); + + const MethodChannel _channel = MethodChannel('OneSignalBackground'); + + // This is where we handle background events from the native portion of the plugin. + _channel.setMethodCallHandler((MethodCall call) async { + if (call.method == 'OneSignal#onBackgroundNotification') { + final CallbackHandle notificationCallbackHandle = + CallbackHandle.fromRawHandle( + call.arguments['notificationCallbackHandle']); + + // PluginUtilities.getCallbackFromHandle performs a lookup based on the + // callback handle and returns a tear-off of the original callback. + final closure = + PluginUtilities.getCallbackFromHandle(notificationCallbackHandle)! + as Future Function(OSNotificationReceivedEvent); + + try { + Map messageMap = + Map.from(call.arguments['message']); + final notification = OSNotificationReceivedEvent(messageMap); + await closure(notification); + } catch (e) { + // ignore: avoid_print + print( + 'OneSignal: An error occurred in your background messaging handler:'); + // ignore: avoid_print + print(e); + } + } else { + throw UnimplementedError('${call.method} has not been implemented'); + } + }); + + // Once we've finished initializing, let the native portion of the plugin + // know that it can start scheduling alarms. + _channel.invokeMethod('OneSignal#backgroundHandlerInitialized'); +} + class OneSignal { /// A singleton representing the OneSignal SDK. /// Note that the iOS and Android native libraries are static, @@ -130,12 +173,15 @@ class OneSignal { void setNotificationWillShowHandler( NotificationWillShowInForegroundHandler handler) { - _onNotificationWillShowInForegroundHandler = handler; - // _channel.invokeMethod("OneSignal#initNotificationWillShowInForegroundHandlerParams"); + final CallbackHandle bgHandle = + PluginUtilities.getCallbackHandle(_callbackDispatcher)!; final CallbackHandle notificationHandler = PluginUtilities.getCallbackHandle(handler)!; - _channel.invokeMapMethod("OneSignal#initNotificationWillShowHandlerParams", - {'notificationCallbackHandle': notificationHandler.toRawHandle()}); + _channel + .invokeMapMethod("OneSignal#initNotificationWillShowHandlerParams", { + 'notificationCallbackHandle': notificationHandler.toRawHandle(), + 'pluginCallbackHandle': bgHandle.toRawHandle(), + }); } /// The notification foreground handler is called whenever a notification arrives @@ -460,6 +506,10 @@ class OneSignal { this._onNotificationWillShowInForegroundHandler != null) { this._onNotificationWillShowInForegroundHandler!( OSNotificationReceivedEvent(call.arguments.cast())); + } else if (call.method == 'OneSignal#handleNotificationWillShow' && + this._onNotificationWillShowInForegroundHandler != null) { + this._onNotificationWillShowInForegroundHandler!( + OSNotificationReceivedEvent(call.arguments.cast())); } return null; } From 13514bc43555c41c6a230615f45c4144097420c8 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 13 Aug 2021 16:54:25 +0200 Subject: [PATCH 3/6] feature: using same function for defining same android / ios callack --- lib/onesignal_flutter.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index ed741e13..de0d133d 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -173,6 +174,10 @@ class OneSignal { void setNotificationWillShowHandler( NotificationWillShowInForegroundHandler handler) { + if (Platform.isIOS) { + setNotificationWillShowInForegroundHandler(handler); + return; + } final CallbackHandle bgHandle = PluginUtilities.getCallbackHandle(_callbackDispatcher)!; final CallbackHandle notificationHandler = From 24fac269b297ff759edf486e3bf52b200ee91085 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 13 Aug 2021 17:41:57 +0200 Subject: [PATCH 4/6] feature: fixing logs --- .../onesignal/flutter/BackgroundExecutor.java | 19 ++++++++++--------- .../XNotificationServiceExtension.java | 9 +++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java b/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java index 9a652401..26204825 100644 --- a/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java +++ b/android/src/main/java/com/onesignal/flutter/BackgroundExecutor.java @@ -34,7 +34,9 @@ * callback dispatcher, used to invoke Dart callbacks while backgrounded. */ public class BackgroundExecutor implements MethodCallHandler { - private static final String TAG = "BackgroundExecutor"; + private static final String TAG = "OneSignal - BackgroundExecutor"; + private static final String CHANNEL = "OneSignalBackground"; + private static final String OSK = "one_signal_key"; private static final String CALLBACK_HANDLE_KEY = "callback_handle"; private static final String USER_CALLBACK_HANDLE_KEY = "user_callback_handle"; @@ -69,7 +71,7 @@ public static void setPluginRegistrant( public static void setCallbackDispatcher(long callbackHandle) { Context context = ContextHolder.getApplicationContext(); SharedPreferences prefs = - context.getSharedPreferences("one_signal_key", 0); + context.getSharedPreferences(OSK, 0); prefs.edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply(); } @@ -82,7 +84,7 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { // is running. From this point forward, the Android side of this plugin can send // callback handles through the background method channel, and the Dart side will execute // the Dart methods corresponding to those callback handles. - Log.i("OneSignal", "Background channel ready"); + Log.i(TAG, "Background channel ready"); result.success(true); } else { @@ -128,7 +130,7 @@ private void onInitialized() { * */ public void startBackgroundIsolate(IsolateStatusHandler isolate) { - Log.i("OneSignal", "Starting background isolate."); + Log.i(TAG, "Starting background isolate."); if (isNotRunning()) { long callbackHandle = getPluginCallbackHandle(); if (callbackHandle != 0) { @@ -268,7 +270,7 @@ private long getUserCallbackHandle() { } SharedPreferences prefs = ContextHolder.getApplicationContext() - .getSharedPreferences("one_signal_key", 0); + .getSharedPreferences(OSK, 0); return prefs.getLong(USER_CALLBACK_HANDLE_KEY, 0); } @@ -279,7 +281,7 @@ private long getUserCallbackHandle() { public static void setUserCallbackHandle(long callbackHandle) { Context context = ContextHolder.getApplicationContext(); SharedPreferences prefs = - context.getSharedPreferences("one_signal_key", 0); + context.getSharedPreferences(OSK, 0); prefs.edit().putLong(USER_CALLBACK_HANDLE_KEY, callbackHandle).apply(); } @@ -290,15 +292,14 @@ private long getPluginCallbackHandle() { } SharedPreferences prefs = ContextHolder.getApplicationContext() - .getSharedPreferences("one_signal_key", 0); + .getSharedPreferences(OSK, 0); return prefs.getLong(CALLBACK_HANDLE_KEY, 0); } // This channel is responsible for sending requests from Android to Dart to execute Dart // callbacks in the background isolate. private void initializeMethodChannel(BinaryMessenger isolate) { - backgroundChannel = - new MethodChannel(isolate, "OneSignalBackground"); + backgroundChannel = new MethodChannel(isolate, CHANNEL); backgroundChannel.setMethodCallHandler(this); } } \ No newline at end of file diff --git a/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java b/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java index 16f37695..3125f313 100644 --- a/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java +++ b/android/src/main/java/com/onesignal/flutter/XNotificationServiceExtension.java @@ -21,6 +21,7 @@ public class XNotificationServiceExtension implements OSRemoteNotificationReceivedHandler { + private static final String TAG = "OneSignal - XNotificationServiceExtension"; public static BackgroundExecutor be; public static HashMap notificationReceivedEventCache = new HashMap<>(); @@ -29,8 +30,7 @@ public void remoteNotificationReceived(Context context, OSNotificationReceivedEv OSNotification notification = notificationReceivedEvent.getNotification(); XNotificationServiceExtension.notificationReceivedEventCache.put(notification.getNotificationId(), notificationReceivedEvent); JSONObject data = notification.getAdditionalData(); - Log.i("OneSignal", "Received Notification Data: " + data.toString()); - // notificationReceivedEvent.complete(null); + Log.i(TAG, "Received Notification Data: " + data.toString()); if (ContextHolder.getApplicationContext() == null) { ContextHolder.setApplicationContext(context.getApplicationContext()); @@ -38,19 +38,20 @@ public void remoteNotificationReceived(Context context, OSNotificationReceivedEv if (XNotificationServiceExtension.be == null) { XNotificationServiceExtension.be = new BackgroundExecutor(); } - Log.i("OneSignal", "Checking isolated BackgroundExecutor."); + Log.i(TAG, "Checking isolated BackgroundExecutor."); try { HashMap receivedMap = OneSignalSerializer.convertNotificationReceivedEventToMap(notificationReceivedEvent); XNotificationServiceExtension.be.startBackgroundIsolate(new IsolateStatusHandler() { @Override public void done() { if (XNotificationServiceExtension.be != null) { + Log.i(TAG, "Executing dart code in isolate."); XNotificationServiceExtension.be.executeDartCallbackInBackgroundIsolate(receivedMap); } } }); } catch (Exception e) { - Log.i("OneSignal", "Exception on XNotificationServiceExtension", e); + Log.i(TAG, "Exception while executing dart code in isolate", e); } } } \ No newline at end of file From 3288829e8d555a12f99477f6a3e52f1f163b0d43 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 15 Aug 2021 04:48:25 +0200 Subject: [PATCH 5/6] feature: [IOS] notification service extension not working --- example/.flutter-plugins-dependencies | 2 +- ios/Classes/OneSignalPlugin.h | 2 + ios/Classes/OneSignalPlugin.m | 94 +++++++++++++++++++ .../lib/XOneSignalNotificationService.h | 13 +++ .../lib/XOneSignalNotificationService.m | 45 +++++++++ lib/onesignal_flutter.dart | 4 +- 6 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 ios/Classes/lib/XOneSignalNotificationService.h create mode 100644 ios/Classes/lib/XOneSignalNotificationService.m diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 3db6583e..48b663e9 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-12 01:52:39.082971","version":"2.2.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-13 17:25:40.930142","version":"2.2.3"} \ No newline at end of file diff --git a/ios/Classes/OneSignalPlugin.h b/ios/Classes/OneSignalPlugin.h index 2d075914..b822550f 100644 --- a/ios/Classes/OneSignalPlugin.h +++ b/ios/Classes/OneSignalPlugin.h @@ -30,6 +30,8 @@ @interface OneSignalPlugin : NSObject +@property (strong, nonatomic) FlutterMethodChannel *callbackChannel; + // Do NOT initialize instances of this class. // You must only reference the shared instance. + (instancetype)sharedInstance; diff --git a/ios/Classes/OneSignalPlugin.m b/ios/Classes/OneSignalPlugin.m index 756d084c..857742bf 100644 --- a/ios/Classes/OneSignalPlugin.m +++ b/ios/Classes/OneSignalPlugin.m @@ -26,6 +26,7 @@ */ #import "OneSignalPlugin.h" +#import "XOneSignalNotificationService.h" #import "OSFlutterCategories.h" #import "OSFlutterTagsController.h" #import "OSFlutterInAppMessagesController.h" @@ -35,6 +36,8 @@ @interface OneSignalPlugin () @property (strong, nonatomic) FlutterMethodChannel *channel; +@property (strong, nonatomic) FlutterEngine *headlessRunner; +@property (strong, nonatomic) NSObject *registrar; /* Will be true if the SDK is waiting for the user's consent before initializing. @@ -56,6 +59,8 @@ it will add the observers (ie. subscription) @property (strong, nonatomic) NSMutableDictionary* notificationCompletionCache; @property (strong, nonatomic) NSMutableDictionary* receivedNotificationCache; +@property (strong, nonatomic) NSUserDefaults *persistentState; + @end @implementation OneSignalPlugin @@ -74,6 +79,9 @@ + (instancetype)sharedInstance { return sharedInstance; } +static FlutterPluginRegistrantCallback registerPlugins = nil; +static BOOL backgroundIsolateRun = NO; + #pragma mark FlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { @@ -88,13 +96,36 @@ + (void)registerWithRegistrar:(NSObject*)registrar { methodChannelWithName:@"OneSignal" binaryMessenger:[registrar messenger]]; + OneSignalPlugin.sharedInstance.registrar = registrar; + OneSignalPlugin.sharedInstance.headlessRunner = [[FlutterEngine alloc] + initWithName:@"OneSignalHeadlessRunner" + project:nil + allowHeadlessExecution:YES]; + + OneSignalPlugin.sharedInstance.persistentState = [NSUserDefaults standardUserDefaults]; + + + OneSignalPlugin.sharedInstance.callbackChannel = + [FlutterMethodChannel methodChannelWithName:@"OneSignalBackground" + binaryMessenger:OneSignalPlugin.sharedInstance.headlessRunner.binaryMessenger]; + [registrar addMethodCallDelegate:OneSignalPlugin.sharedInstance channel:OneSignalPlugin.sharedInstance.channel]; + // [registrar addMethodCallDelegate:OneSignalPlugin.sharedInstance channel:OneSignalPlugin.sharedInstance.callbackChannel]; [OSFlutterTagsController registerWithRegistrar:registrar]; [OSFlutterInAppMessagesController registerWithRegistrar:registrar]; [OSFlutterOutcomeEventsController registerWithRegistrar:registrar]; } ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + registerPlugins = callback; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [self initNotificationWillShowHandlerParamsById:[self getCallbackDispatcherHandle] withNotification: [self getCallbackNotificationHandle]]; + return YES; +} + - (void)addObservers { [OneSignal addSubscriptionObserver:self]; [OneSignal addPermissionObserver:self]; @@ -150,8 +181,12 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self initInAppMessageClickedHandlerParams]; else if ([@"OneSignal#initNotificationWillShowInForegroundHandlerParams" isEqualToString:call.method]) [self initNotificationWillShowInForegroundHandlerParams]; + else if ([@"OneSignal#initNotificationWillShowHandlerParams" isEqualToString:call.method]) + [self initNotificationWillShowHandlerParams:call withResult:result]; else if ([@"OneSignal#completeNotification" isEqualToString:call.method]) [self completeNotification:call withResult:result]; + else if ([@"OneSignal#backgroundHandlerInitialized" isEqualToString:call.method]) + NSLog(@"Background channel ready"); else result(FlutterMethodNotImplemented); } @@ -350,6 +385,65 @@ - (void)initNotificationWillShowInForegroundHandlerParams { self.hasSetNotificationWillShowInForegroundHandler = YES; } +- (int64_t)getCallbackDispatcherHandle { + id handle = [OneSignalPlugin.sharedInstance.persistentState objectForKey:@"callback_dispatcher_handle"]; + if (handle == nil) { + return 0; + } + return [handle longLongValue]; +} + +- (void)setCallbackDispatcherHandle:(int64_t)handle { + [OneSignalPlugin.sharedInstance.persistentState setObject:[NSNumber numberWithLongLong:handle] + forKey:@"callback_dispatcher_handle"]; +} + +- (int64_t)getCallbackNotificationHandle { + id handle = [OneSignalPlugin.sharedInstance.persistentState objectForKey:@"callback_notification_handle"]; + if (handle == nil) { + return 0; + } + return [handle longLongValue]; +} + +- (void)setCallbackNotificationHandle:(int64_t)handle { + [OneSignalPlugin.sharedInstance.persistentState setObject:[NSNumber numberWithLongLong:handle] + forKey:@"callback_notification_handle"]; +} + +- (void)initNotificationWillShowHandlerParams:(FlutterMethodCall*)call withResult:(FlutterResult)result { + int64_t pluginCallbackHandle = [call.arguments[@"pluginCallbackHandle"] longValue]; + int64_t notificationCallbackHandle = [call.arguments[@"notificationCallbackHandle"] longValue]; + [self initNotificationWillShowHandlerParamsById:pluginCallbackHandle withNotification: notificationCallbackHandle]; +} + +- (void)initNotificationWillShowHandlerParamsById:(int64_t)dispatcherCallbackHandle withNotification: (int64_t)notificationCallbackHandle { + NSLog(@"Initializing Headless Runner"); + + NSAssert(dispatcherCallbackHandle != 0, @"dispatcherCallbackHandle can't be 0"); + NSAssert(notificationCallbackHandle != 0, @"notificationCallbackHandle can't be 0"); + + [self setCallbackDispatcherHandle:dispatcherCallbackHandle]; + [self setCallbackNotificationHandle:notificationCallbackHandle]; + + FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:dispatcherCallbackHandle]; + NSAssert(info != nil, @"failed to find callback"); + + NSString *entrypoint = info.callbackName; + NSString *uri = info.callbackLibraryPath; + [OneSignalPlugin.sharedInstance.headlessRunner runWithEntrypoint:entrypoint libraryURI:uri]; + + // Once our headless runner has been started, we need to register the application's plugins + // with the runner in order for them to work on the background isolate. `registerPlugins` is + // a callback set from AppDelegate.m in the main application. This callback should register + // all relevant plugins (excluding those which require UI). + [OneSignalPlugin.sharedInstance.registrar addMethodCallDelegate:self channel:OneSignalPlugin.sharedInstance.callbackChannel]; + if (!backgroundIsolateRun && registerPlugins != nil) { + registerPlugins(OneSignalPlugin.sharedInstance.headlessRunner); + } + backgroundIsolateRun = YES; +} + #pragma mark Opened Notification Handlers - (void)handleNotificationOpened:(OSNotificationOpenedResult *)result { [self.channel invokeMethod:@"OneSignal#handleOpenedNotification" arguments:result.toJson]; diff --git a/ios/Classes/lib/XOneSignalNotificationService.h b/ios/Classes/lib/XOneSignalNotificationService.h new file mode 100644 index 00000000..9bd31938 --- /dev/null +++ b/ios/Classes/lib/XOneSignalNotificationService.h @@ -0,0 +1,13 @@ +// +// NotificationService.h +// OneSignalNotificationServiceExtension +// +// Created by Brad Hesse on 7/13/18. +// Copyright © 2018 The Chromium Authors. All rights reserved. +// + +#import + +@interface XOneSignalNotificationService : UNNotificationServiceExtension + +@end diff --git a/ios/Classes/lib/XOneSignalNotificationService.m b/ios/Classes/lib/XOneSignalNotificationService.m new file mode 100644 index 00000000..95da42e3 --- /dev/null +++ b/ios/Classes/lib/XOneSignalNotificationService.m @@ -0,0 +1,45 @@ +// +// NotificationService.m +// OneSignalNotificationServiceExtension +// +// Created by Brad Hesse on 7/13/18. +// Copyright © 2018 The Chromium Authors. All rights reserved. +// + +#import +#import "OneSignalPlugin.h" + +#import "XOneSignalNotificationService.h" + +static XOneSignalNotificationService *obj; + +@interface XOneSignalNotificationService () + +@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); +@property (nonatomic, strong) UNNotificationRequest *receivedRequest; +@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; + +@end + +@implementation XOneSignalNotificationService + +- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { + self.receivedRequest = request; + self.contentHandler = contentHandler; + self.bestAttemptContent = [request.content mutableCopy]; + NSLog(@"didReceiveNotificationRequest"); + [OneSignalPlugin.sharedInstance.callbackChannel invokeMethod: @"OneSignal#onBackgroundNotification" arguments: self.bestAttemptContent]; + + // [OneSignal didReceiveNotificationExtensionRequest:self.receivedRequest withMutableNotificationContent:self.bestAttemptContent withContentHandler:self.contentHandler]; +} + +- (void)serviceExtensionTimeWillExpire { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + + [OneSignal serviceExtensionTimeWillExpireRequest:self.receivedRequest withMutableNotificationContent:self.bestAttemptContent]; + + self.contentHandler(self.bestAttemptContent); +} + +@end diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index de0d133d..ea206854 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -70,8 +70,7 @@ void _callbackDispatcher() { } }); - // Once we've finished initializing, let the native portion of the plugin - // know that it can start scheduling alarms. + print("OneSignalPlugin - plugin callback ready"); _channel.invokeMethod('OneSignal#backgroundHandlerInitialized'); } @@ -176,7 +175,6 @@ class OneSignal { NotificationWillShowInForegroundHandler handler) { if (Platform.isIOS) { setNotificationWillShowInForegroundHandler(handler); - return; } final CallbackHandle bgHandle = PluginUtilities.getCallbackHandle(_callbackDispatcher)!; From f692577c318a088a4a5445220856e0beac2e6199 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 18 Aug 2021 16:28:53 +0200 Subject: [PATCH 6/6] Revert "feature: [IOS] notification service extension not working" This reverts commit 3288829e8d555a12f99477f6a3e52f1f163b0d43. --- example/.flutter-plugins-dependencies | 2 +- ios/Classes/OneSignalPlugin.h | 2 - ios/Classes/OneSignalPlugin.m | 94 ------------------- .../lib/XOneSignalNotificationService.h | 13 --- .../lib/XOneSignalNotificationService.m | 45 --------- lib/onesignal_flutter.dart | 4 +- 6 files changed, 4 insertions(+), 156 deletions(-) delete mode 100644 ios/Classes/lib/XOneSignalNotificationService.h delete mode 100644 ios/Classes/lib/XOneSignalNotificationService.m diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 48b663e9..3db6583e 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-13 17:25:40.930142","version":"2.2.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"android":[{"name":"onesignal_flutter","path":"/Users/efraespada/landa-workspace/OneSignal-Flutter-SDK/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"onesignal_flutter","dependencies":[]}],"date_created":"2021-08-12 01:52:39.082971","version":"2.2.3"} \ No newline at end of file diff --git a/ios/Classes/OneSignalPlugin.h b/ios/Classes/OneSignalPlugin.h index b822550f..2d075914 100644 --- a/ios/Classes/OneSignalPlugin.h +++ b/ios/Classes/OneSignalPlugin.h @@ -30,8 +30,6 @@ @interface OneSignalPlugin : NSObject -@property (strong, nonatomic) FlutterMethodChannel *callbackChannel; - // Do NOT initialize instances of this class. // You must only reference the shared instance. + (instancetype)sharedInstance; diff --git a/ios/Classes/OneSignalPlugin.m b/ios/Classes/OneSignalPlugin.m index 857742bf..756d084c 100644 --- a/ios/Classes/OneSignalPlugin.m +++ b/ios/Classes/OneSignalPlugin.m @@ -26,7 +26,6 @@ */ #import "OneSignalPlugin.h" -#import "XOneSignalNotificationService.h" #import "OSFlutterCategories.h" #import "OSFlutterTagsController.h" #import "OSFlutterInAppMessagesController.h" @@ -36,8 +35,6 @@ @interface OneSignalPlugin () @property (strong, nonatomic) FlutterMethodChannel *channel; -@property (strong, nonatomic) FlutterEngine *headlessRunner; -@property (strong, nonatomic) NSObject *registrar; /* Will be true if the SDK is waiting for the user's consent before initializing. @@ -59,8 +56,6 @@ it will add the observers (ie. subscription) @property (strong, nonatomic) NSMutableDictionary* notificationCompletionCache; @property (strong, nonatomic) NSMutableDictionary* receivedNotificationCache; -@property (strong, nonatomic) NSUserDefaults *persistentState; - @end @implementation OneSignalPlugin @@ -79,9 +74,6 @@ + (instancetype)sharedInstance { return sharedInstance; } -static FlutterPluginRegistrantCallback registerPlugins = nil; -static BOOL backgroundIsolateRun = NO; - #pragma mark FlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { @@ -96,36 +88,13 @@ + (void)registerWithRegistrar:(NSObject*)registrar { methodChannelWithName:@"OneSignal" binaryMessenger:[registrar messenger]]; - OneSignalPlugin.sharedInstance.registrar = registrar; - OneSignalPlugin.sharedInstance.headlessRunner = [[FlutterEngine alloc] - initWithName:@"OneSignalHeadlessRunner" - project:nil - allowHeadlessExecution:YES]; - - OneSignalPlugin.sharedInstance.persistentState = [NSUserDefaults standardUserDefaults]; - - - OneSignalPlugin.sharedInstance.callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"OneSignalBackground" - binaryMessenger:OneSignalPlugin.sharedInstance.headlessRunner.binaryMessenger]; - [registrar addMethodCallDelegate:OneSignalPlugin.sharedInstance channel:OneSignalPlugin.sharedInstance.channel]; - // [registrar addMethodCallDelegate:OneSignalPlugin.sharedInstance channel:OneSignalPlugin.sharedInstance.callbackChannel]; [OSFlutterTagsController registerWithRegistrar:registrar]; [OSFlutterInAppMessagesController registerWithRegistrar:registrar]; [OSFlutterOutcomeEventsController registerWithRegistrar:registrar]; } -+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { - registerPlugins = callback; -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [self initNotificationWillShowHandlerParamsById:[self getCallbackDispatcherHandle] withNotification: [self getCallbackNotificationHandle]]; - return YES; -} - - (void)addObservers { [OneSignal addSubscriptionObserver:self]; [OneSignal addPermissionObserver:self]; @@ -181,12 +150,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self initInAppMessageClickedHandlerParams]; else if ([@"OneSignal#initNotificationWillShowInForegroundHandlerParams" isEqualToString:call.method]) [self initNotificationWillShowInForegroundHandlerParams]; - else if ([@"OneSignal#initNotificationWillShowHandlerParams" isEqualToString:call.method]) - [self initNotificationWillShowHandlerParams:call withResult:result]; else if ([@"OneSignal#completeNotification" isEqualToString:call.method]) [self completeNotification:call withResult:result]; - else if ([@"OneSignal#backgroundHandlerInitialized" isEqualToString:call.method]) - NSLog(@"Background channel ready"); else result(FlutterMethodNotImplemented); } @@ -385,65 +350,6 @@ - (void)initNotificationWillShowInForegroundHandlerParams { self.hasSetNotificationWillShowInForegroundHandler = YES; } -- (int64_t)getCallbackDispatcherHandle { - id handle = [OneSignalPlugin.sharedInstance.persistentState objectForKey:@"callback_dispatcher_handle"]; - if (handle == nil) { - return 0; - } - return [handle longLongValue]; -} - -- (void)setCallbackDispatcherHandle:(int64_t)handle { - [OneSignalPlugin.sharedInstance.persistentState setObject:[NSNumber numberWithLongLong:handle] - forKey:@"callback_dispatcher_handle"]; -} - -- (int64_t)getCallbackNotificationHandle { - id handle = [OneSignalPlugin.sharedInstance.persistentState objectForKey:@"callback_notification_handle"]; - if (handle == nil) { - return 0; - } - return [handle longLongValue]; -} - -- (void)setCallbackNotificationHandle:(int64_t)handle { - [OneSignalPlugin.sharedInstance.persistentState setObject:[NSNumber numberWithLongLong:handle] - forKey:@"callback_notification_handle"]; -} - -- (void)initNotificationWillShowHandlerParams:(FlutterMethodCall*)call withResult:(FlutterResult)result { - int64_t pluginCallbackHandle = [call.arguments[@"pluginCallbackHandle"] longValue]; - int64_t notificationCallbackHandle = [call.arguments[@"notificationCallbackHandle"] longValue]; - [self initNotificationWillShowHandlerParamsById:pluginCallbackHandle withNotification: notificationCallbackHandle]; -} - -- (void)initNotificationWillShowHandlerParamsById:(int64_t)dispatcherCallbackHandle withNotification: (int64_t)notificationCallbackHandle { - NSLog(@"Initializing Headless Runner"); - - NSAssert(dispatcherCallbackHandle != 0, @"dispatcherCallbackHandle can't be 0"); - NSAssert(notificationCallbackHandle != 0, @"notificationCallbackHandle can't be 0"); - - [self setCallbackDispatcherHandle:dispatcherCallbackHandle]; - [self setCallbackNotificationHandle:notificationCallbackHandle]; - - FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:dispatcherCallbackHandle]; - NSAssert(info != nil, @"failed to find callback"); - - NSString *entrypoint = info.callbackName; - NSString *uri = info.callbackLibraryPath; - [OneSignalPlugin.sharedInstance.headlessRunner runWithEntrypoint:entrypoint libraryURI:uri]; - - // Once our headless runner has been started, we need to register the application's plugins - // with the runner in order for them to work on the background isolate. `registerPlugins` is - // a callback set from AppDelegate.m in the main application. This callback should register - // all relevant plugins (excluding those which require UI). - [OneSignalPlugin.sharedInstance.registrar addMethodCallDelegate:self channel:OneSignalPlugin.sharedInstance.callbackChannel]; - if (!backgroundIsolateRun && registerPlugins != nil) { - registerPlugins(OneSignalPlugin.sharedInstance.headlessRunner); - } - backgroundIsolateRun = YES; -} - #pragma mark Opened Notification Handlers - (void)handleNotificationOpened:(OSNotificationOpenedResult *)result { [self.channel invokeMethod:@"OneSignal#handleOpenedNotification" arguments:result.toJson]; diff --git a/ios/Classes/lib/XOneSignalNotificationService.h b/ios/Classes/lib/XOneSignalNotificationService.h deleted file mode 100644 index 9bd31938..00000000 --- a/ios/Classes/lib/XOneSignalNotificationService.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// NotificationService.h -// OneSignalNotificationServiceExtension -// -// Created by Brad Hesse on 7/13/18. -// Copyright © 2018 The Chromium Authors. All rights reserved. -// - -#import - -@interface XOneSignalNotificationService : UNNotificationServiceExtension - -@end diff --git a/ios/Classes/lib/XOneSignalNotificationService.m b/ios/Classes/lib/XOneSignalNotificationService.m deleted file mode 100644 index 95da42e3..00000000 --- a/ios/Classes/lib/XOneSignalNotificationService.m +++ /dev/null @@ -1,45 +0,0 @@ -// -// NotificationService.m -// OneSignalNotificationServiceExtension -// -// Created by Brad Hesse on 7/13/18. -// Copyright © 2018 The Chromium Authors. All rights reserved. -// - -#import -#import "OneSignalPlugin.h" - -#import "XOneSignalNotificationService.h" - -static XOneSignalNotificationService *obj; - -@interface XOneSignalNotificationService () - -@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); -@property (nonatomic, strong) UNNotificationRequest *receivedRequest; -@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; - -@end - -@implementation XOneSignalNotificationService - -- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { - self.receivedRequest = request; - self.contentHandler = contentHandler; - self.bestAttemptContent = [request.content mutableCopy]; - NSLog(@"didReceiveNotificationRequest"); - [OneSignalPlugin.sharedInstance.callbackChannel invokeMethod: @"OneSignal#onBackgroundNotification" arguments: self.bestAttemptContent]; - - // [OneSignal didReceiveNotificationExtensionRequest:self.receivedRequest withMutableNotificationContent:self.bestAttemptContent withContentHandler:self.contentHandler]; -} - -- (void)serviceExtensionTimeWillExpire { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - - [OneSignal serviceExtensionTimeWillExpireRequest:self.receivedRequest withMutableNotificationContent:self.bestAttemptContent]; - - self.contentHandler(self.bestAttemptContent); -} - -@end diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index ea206854..de0d133d 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -70,7 +70,8 @@ void _callbackDispatcher() { } }); - print("OneSignalPlugin - plugin callback ready"); + // Once we've finished initializing, let the native portion of the plugin + // know that it can start scheduling alarms. _channel.invokeMethod('OneSignal#backgroundHandlerInitialized'); } @@ -175,6 +176,7 @@ class OneSignal { NotificationWillShowInForegroundHandler handler) { if (Platform.isIOS) { setNotificationWillShowInForegroundHandler(handler); + return; } final CallbackHandle bgHandle = PluginUtilities.getCallbackHandle(_callbackDispatcher)!;