I have a Xamarin.Forms app that uses push notifications. For some reason, the following line:
(App.Current.MainPage as MainPage)?.AddMessage(body);
that is called from Android native OnMessageReceived(), throws NullReferenceException.
Why can this happen? Isn't App.Current to be accessible from the platform-specific project?
Edit:
Here is the full OnMessageReceived() code:
public override void OnMessageReceived(RemoteMessage message)
{
base.OnMessageReceived(message);
string messageBody;
if (message.GetNotification() != null)
{
messageBody = message.GetNotification().Body;
}
// NOTE: test messages sent via the Azure portal will be received here
else
{
messageBody = message.Data.Values.First();
}
// convert the incoming message to a local notification
SendLocalNotification(messageBody);
// send the incoming message directly to the MainPage
SendMessageToMainPage(messageBody);
}
App.Current.MainPage may contain any Page, not necessary your type MainPage, for example it could be NavigationPage. As the result of casting is null it is clear that it is of some other type.
Also it could happen that nothing is assigned to it.
Related
This is my function in flutter
String getRideRequestId(Map<String,dynamic> message){
String rideRequestId="";
if(Platform.isAndroid){
rideRequestId=message['data']['ride_request_id'];
}else{
rideRequestId=message['ride_request_id'];
}
return rideRequestId;
}
and i wanted to call it in on message listen or onresume
Please check the docs right here: https://firebase.flutter.dev/docs/messaging/usage#foreground-messages
And I guess you're trying to call the getRideRequestId with the message.data property on the RemoteMessage object.
Just do it inside the listener function.
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Got a message whilst in the foreground!');
getRideRequestId(message.data);
});
Please be more specific about the issue you're facing, like do you get any errors while trying this.
I'm making a plugin for Flutter to handle fcm messages using an android native library.
As we know when a message is received by FCM, it starts the app (It's application class) and runs the codes within Application#onCreate block, so we can run native code when app starts by fcm in the background.
My question is, is it possible to run flutter code at that time when application starts?
For instance if the message was received:
Application class:
public class Application extends FlutterApplication {
#Override
public void onCreate() {
super.onCreate();
// Start flutter engine
// Invoke a dart code in the Plugin using methodChannel or etc.
}
}
Short answer, Yes
You can call a Dart method in background using it's handle key.
1. Register your plugin in the background
Implement a custom application class (override FlutterApplication)
public class MyApp extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {
#Override
public void registerWith(io.flutter.plugin.common.PluginRegistry registry) {
// For apps using FlutterEmbedding v1
GeneratedPluginRegistrant.registerWith(registry);
// App with V2 will initialize plugins automatically, you might need to register your own however
}
}
Remember to register the class in the AndroidManifest by adding android:name=".MyApp" to <application> attributes.
What is embedding v2?
2. Create a setup function as top level function in your flutter code
/// Define this TopLevel or static
void _setup() async {
MethodChannel backgroundChannel = const MethodChannel('flutter_background');
// Setup Flutter state needed for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
// This is where the magic happens and we handle background events from the
// native portion of the plugin.
backgroundChannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'handleBackgroundMessage') {
final CallbackHandle handle =
CallbackHandle.fromRawHandle(call.arguments['handle']);
final Function handlerFunction =
PluginUtilities.getCallbackFromHandle(handle);
try {
var dataArg = call.arguments['message'];
if (dataArg == null) {
print('Data received from callback is null');
return;
}
await handlerFunction(dataArg);
} catch (e) {
print('Unable to handle incoming background message.\n$e');
}
}
return Future.value();
});
3. Create a top level callback that will get the background message and calls it
_bgFunction(dynamic message) {
// Message received in background
// Remember, this will be a different isolate. So, no widgets
}
4. Get the handle key of the background function and setup and send it to native via MethodChannel
// dart:ui needed
CallbackHandle setup PluginUtilities.getCallbackHandle(_setup);
CallbackHandle handle PluginUtilities.getCallbackHandle(_bgFunction);
_channel.invokeMethod<bool>(
'handleFunction',
<String, dynamic>{
'handle': handle.toRawHandle(),
'setup': setup.toRawHandle()
},
);
5. Save them into SharedPref in the native side
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
String methodName = call.method
if (methodName == "handleFunction") {
long handle = call.argument("handle");
long setup = call.argument("setup");
// save them
}
}
6. When background is awaken, start a background isolate
FlutterMain.ensureInitializationComplete(context, null)
val appBundlePath = FlutterMain.findAppBundlePath()
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(setupHandleYouHadSaved)
FlutterNativeView backgroundFlutterView = FlutterNativeView(context, true)
val args = FlutterRunArguments()
args.bundlePath = appBundlePath
args.entrypoint = flutterCallback.callbackName
args.libraryPath = flutterCallback.callbackLibraryPath
backgroundFlutterView?.runFromBundle(args)
// Initialize your registrant in the app class
pluginRegistrantCallback?.registerWith(backgroundFlutterView?.pluginRegistry)
7. When your plugin is registered, create a background channel and pass it to
val backgroundChannel = MethodChannel(messenger, "pushe_flutter_background")
8. Call the setup method that would call and give the message to you callback
private fun sendBackgroundMessageToExecute(context: Context, message: String) {
if (backgroundChannel == null) {
return
}
val args: MutableMap<String, Any?> = HashMap()
if (backgroundMessageHandle == null) {
backgroundMessageHandle = getMessageHandle(context)
}
args["handle"] = backgroundMessageHandle
args["message"] = message
// The created background channel at step 7
backgroundChannel?.invokeMethod("handleBackgroundMessage", args, null)
}
The sendBackgroundMessageToExecute will execute the dart _setup function and pass the message and callback handle. In the step 2, callback will be called.
Note: There are still certain corner cases you may want to consider (for instance thread waiting and ...). Checkout the samples and see the source code.
There are several projects which support background execution when app is started in the background.
FirebaseMessaging
Pushe
WorkManager
I did it a different, simpler way compared to Mahdi's answer. I avoided defining an additional entrypoint/ callback, using PluginUtilities, callback handles, saving handles in SharedPreferences, passing messages with handles between dart and platform, or implementing a FlutterApplication.
I was working on a flutter plugin (so you don't have to worry about this if you use my library for push notifications 😂), so I implement FlutterPlugin. If I want to do background processing and the Flutter app isn't running, I just launch the Flutter app without an Activity or View. This is only necessary on Android, since the FlutterEngine/ main dart function runs already runs when a background message is received in an iOS app. The benefit is that this is the same behaviour as iOS: a Flutter app is always running when the app is launched, even if there is no app shown to the user.
I launch the application by using:
flutterEngine = new FlutterEngine(context, null);
DartExecutor executor = flutterEngine.getDartExecutor();
backgroundMethodChannel = new MethodChannel(executor, "com.example.package.background");
backgroundMethodChannel.setMethodCallHandler(this);
// Get and launch the users app isolate manually:
executor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());
I did this to implement background push notification handling in a library, ably_flutter. It seems to work well. The FlutterEngine/ application is launched only when the application is not already running. I do this by keeping track of the activity (using ActivityAware):
if (isApplicationInForeground) {
// Send message to Dart side app already running
Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else if (AblyFlutterPlugin.isActivityRunning) {
// Flutter is already running, just send a background message to it.
Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else {
// No existing Flutter Activity is running, create a FlutterEngine and pass it the RemoteMessage
new PushBackgroundIsolateRunner(context, asyncCompletionHandlerPendingResult, message);
}
Then, I just use a separate MethodChannel to pass the messages back to the dart side. There's more to this parallel processing (like telling the Java side that the App is running/ ready. Search for call.method.equals(pushSetOnBackgroundMessage) in the codebase.). You can see more about the implementation PushBackgroundIsolateRunner.java at ably_flutter. I also used goAsync inside the broadcast receiver to extend the execution time from 10s to 30s, to be consistent with iOS 30s wall clock time.
You can use a headless Runner to run dart code from an Application class (or service, broadcast receiver etc).
There's a good in depth article on how to implement this: https://medium.com/flutter/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124
According to my knowledge we have to call a class GeneratedPluginRegistrant.registerWith(this); at oncreate method where flutter code has to run.
If you mean you want to run some arbitrary Dart code in the background you can use the this plugin we created which really facilitates the use of background work.
You can register a background job that should be executed at a given point in time and it will call back in to your Dart code where you can run some code in the background.
//Provide a top level function or static function.
//This function will be called by Android and will return the value you provided when you registered the task.
//See below
void callbackDispatcher() {
Workmanager.defaultCallbackDispatcher((echoValue) {
print("Native echoed: $echoValue");
return Future.value(true);
});
}
Workmanager.initialize(callbackDispatcher)
Then you can schedule them.
Workmanager.registerOneOffTask(
"1",
"simpleTask"
);
The String simpleTask will be returned in the callbackDispatcher function once it starts running in the background.
This allows for you to schedule multiple background jobs and identify them by this id.
My app is using a NotificationListener to read out messages from various 3rd party apps, for example WhatsApp.
So far I was able to send a reply if only one chat is unread, the code is below.
However, in the case with WhatsApp, getNotification().actions returns a null object when more than two chats are unread, as the messages are bundled together. As you can see in the pictures below, if the notifications are extended there is an option to send a direct reply as well, therefore I am certain that it is possible to utilize this, also I think apps like PushBullet are using this method.
How could I access the RemoteInput of that notification?
public static ReplyIntentSender sendReply(StatusBarNotification statusBarNotification, String name) {
Notification.Action actions[] = statusBarNotification.getNotification().actions;
for (Notification.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
if (act.title.toString().contains(name)) {
if (act.getRemoteInputs() != null)
return new ReplyIntentSender(act);
}
}
}
return null;
}
public static class ReplyIntentSender {
[...]
public final Notification.Action action;
public ReplyIntentSender(Notification.Action extractedAction) {
action = extractedAction;
[...]
}
private boolean sendNativeIntent(Context context, String message) {
for (android.app.RemoteInput rem : action.getRemoteInputs()) {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putCharSequence(rem.getResultKey(), message);
android.app.RemoteInput.addResultsToIntent(action.getRemoteInputs(), intent, bundle);
try {
action.actionIntent.send(context, 0, intent);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
return false;
}
}
Some explanation how the above code works: Once a notification is received the app tries to get the actions and checks if the name is in the title of a remoteInput (normally it is in the format of "Reply to $NAME"), if that is found the Action is saved into a ReplyIntentSender class, which, when triggered by sendNativeIntent, cycles through all RemoteInputs of that Action and adds the message to the intent. If more than one chat is unread, getNotification().actions returns null.
Below are two screenshots, the first one where it is working without any problems and the second one where it doesn't.
You can consider this as my suggestion. I have done bit research on this and come up with following conclusions.(Also it looks like you have done plenty of research on this so it might be possible that you aware about what I wrote below)
Numerous apps send Wear specific notifications, and many of those contain actions accessible from an Android Wear device. We can grab those Wear notifications on the device, extracting the actions, finding the reply action (if one exists), populating it with our own response and then executing the PendingIntent which sends our response back the original app for it to send on to the recipient.
To do so you can refer this link (A nice workaround by Rob J). You can also refer this link in this context (Great research work done by Michał Tajchert).(You might need to work around with NotificationCompat.isGroupSummary)
This is what I feel(Might be I am totally wrong)
.actions method returns Array of all Notification.Action
structures attached to current notification by addAction(int,
CharSequence, PendingIntent), Here addAction method is deprecated
one so it might not working as intended.
I am not able to test this at my end otherwise I will love to provide a working solution with code.
Hope this will help you. Happy Coding!!!
Sinch In-App Instant Messaging works perfectly fine with Sinch Managed Push but except this one issue.
This is the sistuation - I receives messages using GCM Listener when my app is foreground or background and I show notification but except in the case when my app is not running.
I inserted debug logs statements to see the flow and it seems that push message arrives in the GCM Listener and gets sent to my service as well but it never gets relayed to the message client listener. This only happens when the app is not running or is closed.
I am doing the following when the app is running background or foreground and I do get callback in onIncomingMessage but same code doesn't work when app is not running.
Sinch Client Initialization Code:
public void startSinchClient(String username) {
try {
sinchClient = Sinch.getSinchClientBuilder().context(this).userId(username).applicationKey(ApplicationConstants.SINCH_SANDBOX_API_KEY)
.applicationSecret(ApplicationConstants.SINCH_SANDBOX_API_SECRET).environmentHost(ApplicationConstants.SINCH_SANDBOX_API_URL).build();
sinchClient.setSupportMessaging(true);
sinchClient.setSupportManagedPush(true);
sinchClient.checkManifest();
sinchClient.addSinchClientListener(this);
if ( messageClientListener == null ) {
messageClientListener = new MyMessageClientListener();
}
sinchClient.getMessageClient().addMessageClientListener(messageClientListener);
Log.e("SinchMessageService", "Login successful.");
} catch (MissingGCMException missingGCM) {
Log.e("SinchMessageService", missingGCM.getMessage());
}
}
OnBind Code
if (!isSinchClientStarted()) {
startSinchClient(currentUserId);
sinchClient.start();
}
In RelayRemotePushNotificationCode:
public NotificationResult relayRemotePushNotificationPayload(Intent intent) {
if ( currentUserId.isEmpty() ) {
Log.e("SinchMessageService", "UserID not available.Please login again.");
return null;
} else if ( !isSinchClientStarted() ) {
startSinchClient(currentUserId);
sinchClient.start();
}
Log.d("SinchService", "relayRemotePushNotificationPayload");
NotificationResult notificationResult = sinchClient.relayRemotePushNotificationPayload(intent);
if (notificationResult.isMessage()) {
sinchClient.startListeningOnActiveConnection();
}
return notificationResult;
}
In MessageClientListener:
public void onIncomingMessage(MessageClient client, final Message message) {
if (message.getRecipientIds().get(0).equals(ApplicationConstants.userInfo.getEmail())) {
sinchClient.stopListeningOnActiveConnection();
....
The above code works in all the scenarios. I mean when the app is running in foreground as well as background. Only when I kill the app never get the onIncomingMessage callback.
Log statements from Sinch Client:
03-03 22:07:44.213 17381-17381/com.ontyme E/SinchClient: mUserAgent.startBroadcastListener()
03-03 22:07:45.271 17381-17381/com.ontyme E/MessageClient: onIncomingMessage: NativeMessage [id=2059913a-27ac-4105-a797-764f09af66d2, nativeAddress=-1321533856]
Anyone else has faced the issue?
Sorry guys there is no problem with the Sinch Managed Push. It was small typo at my end which was causing this issue. My receipentid in the app was not getting initialized correctly when the app was not running which is why all the messages were getting ignored in onIncomingMessage.
Managed Push works seamlessly for me now.
First off I am using Xamarin Forms for a WP8, iOS and Android app.
Goal:
I want to go to a specific page when the toast is clicked depending
upon the payload information of the toast notification.
I have push notifications using Azure Notification Hubs all setup and working well. I use MVVMLight and their dependency injection to setup push notifications specifically for each platform.
Each payload needs to be sent a little different due to the different formats required. With each you will notice I want to send a SignalId in the payload to perform a different action as required on the receiving device from regular push notifications.
Android
{
"data" : {
"msg" : "message in here",
"signalId" : "id-in-here",
},
}
iOS
{
"aps" : { "alert" : "message in here" },
"signalId" : "id-in-here"
}
Windows Phone 8
<?xml version="1.0" encoding="utf-8"?>
<wp:Notification xmlns:wp="WPNotification">
<wp:Toast>
<wp:Text1>category</wp:Text1>
<wp:Text2>message in here</wp:Text2>
<wp:Param>?signalId=id-in-here</wp:Param>
</wp:Toast>
</wp:Notification>
.
Question:
How do I get this information in a Xamarin Forms app and redirect to
the appropriate page when the application is reactivated because the
user clicked on the toast notification?
I want to get the payload information when the app loads, then say, yes this contains a SignalId, lets redirect to this page.
At the moment all it does it show the application when a toast notification is clicked. Must I do it specific to the app, or is there a Xamarin Forms way?
Any help appreciated even if you only know how to do it for one platform, I can probably work my way around the other platforms from there.
I have found the way to do it for all platforms. Windows has been tested, Android and iOS haven't.
Windows and iOS work on a show toast notification if the app is in the background, or let your code deal with it if the app is in the foreground. Android shows the toast regardless of application status.
With Windows Phone 8 I need to go to the MainPage.xaml.cs and add in this override.
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (this.NavigationContext.QueryString.ContainsKey("signalId"))
{
var signalId = this.NavigationContext.QueryString["signalId"];
var id = Guid.Empty;
if (signalId != null
&& Guid.TryParse(signalId, out id)
&& id != Guid.Empty)
{
this.NavigationContext.QueryString.Clear();
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
// Do my navigation to a new page
});
}
}
}
For Android in the GcmService
protected override void OnMessage(Context context, Intent intent)
{
Log.Info(Tag, "GCM Message Received!");
var message = intent.Extras.Get("msg").ToString();
var signalId = Guid.Empty;
if (intent.Extras.ContainsKey("signalId"))
{
signalId = new Guid(intent.Extras.Get("signalId").ToString());
}
// Show notification as usual
CreateNotification("", message, signalId);
}
Then in the CreateNotification function put some extra information in the Intent.
var uiIntent = new Intent(this, typeof(MainActivity));
if (signalId != Guid.Empty)
{
uiIntent.PutExtra("SignalId", signalId.ToString());
}
Then in the MainActivity.cs override this function
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (data.HasExtra("SignalId"))
{
Guid signalId = new Guid(data.GetStringExtra("SignalId"));
if (signalId != Guid.Empty)
{
data.RemoveExtra("SignalId");
// Do you navigation
}
}
}
In iOS you will notice I have enhanced the default ProcessNotification()
void ProcessNotification(NSDictionary options, bool fromFinishedLaunching)
{
// Check to see if the dictionary has the aps key. This is the notification payload you would have sent
if (null != options && options.ContainsKey(new NSString("aps")))
{
//Get the aps dictionary
var aps = options.ObjectForKey(new NSString("aps")) as NSDictionary;
var alert = string.Empty;
//Extract the alert text
// NOTE: If you're using the simple alert by just specifying
// " aps:{alert:"alert msg here"} " this will work fine.
// But if you're using a complex alert with Localization keys, etc.,
// your "alert" object from the aps dictionary will be another NSDictionary.
// Basically the json gets dumped right into a NSDictionary,
// so keep that in mind.
if (aps.ContainsKey(new NSString("alert")))
alert = ((NSString) aps[new NSString("alert")]).ToString();
// If this came from the ReceivedRemoteNotification while the app was running,
// we of course need to manually process things like the sound, badge, and alert.
if (!fromFinishedLaunching)
{
//Manually show an alert
if (!string.IsNullOrEmpty(alert))
{
var signalId = new Guid(options.ObjectForKey(new NSString("signalId")) as NSString);
// Show my own toast with the signalId
}
}
}
}
Then in the FinishedLaunching function check if there is any payload
// Check if any payload from the push notification
if (options.ContainsKey("signalId"))
{
var signalId = new Guid(options.ObjectForKey(new NSString("signalId")) as NSString);
// Do the navigation here
}