英文:
How can I receive a Push Notification when my Xamarin Android app is stopped?
问题
我看到了许多不同类型的解决方案,这些解决方案在过去可能有效,但对我自己来说没有真正可行的东西。这是一个人们说什么有效,什么无效,什么已经改变等问题的迷宫。但我正在努力不仅找到解决方案,而且希望理解 - 因为现在我真的很困惑。
我现在可以做什么 - 我的Xamarin Forms应用程序(Android)如果应用程序在前台/后台,可以接收推送通知,我还可以拦截用户点击通知时的操作,以便告诉我的应用程序该做什么。
我正在尝试做什么 - 本质上与上面相同,但在应用程序完全停止的状态下。
我已经设置了我的Firebase Messaging,它连接到Azure Notification Hub - 不幸的是,我不会从Azure迁移(以防有人建议放弃它)。我目前拥有的大部分信息都是我从各种Microsoft文档([此处][1],我不使用AppCenter - 只是用它来交叉参考任何有用的代码,[此处][2],[此处][3]和[此处][4])以及其他StackOverflow问题(例如[此处][5],[此处][6]和[此处][7]等等)以及Xamarin论坛中拼凑而来的 - 所以如果使用了任何已过时的代码,我再次道歉(请告诉我 - 我已尽力使用最新的方法等)。
我发送的推送通知类型是数据消息 [我在这里阅读了关于它的信息][8],我在通知中使用自定义数据,因此我理解这是我想要发送的正确类型的推送,如下所示。
{
"data": {
"title": "标题测试",
"body": "推送通知正文测试",
"area": "SelectedPage"
}
}
以下是我目前在项目中设置以处理推送通知的代码。
清单
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.pushtesting" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="Push Testing">
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
MainActivity.cs
我有LaunchMode = LaunchMode.SingleTop
,我理解这是为了确保当前Activity仍然在使用中,而不是创建一个新的Activity - 例如,当用户点击通知时 - 实施它以及附加代码(下面)似乎表明这是正确的。
protected override void OnNewIntent(Intent intent) {
base.OnNewIntent(intent);
string area = string.Empty;
string extraInfo = string.Empty;
if (intent.Extras != null) {
foreach (string key in intent.Extras.KeySet()) {
string value = intent.Extras.GetString(key);
if (key == "Area" && !string.IsNullOrEmpty(value)) {
area = value;
} else if (key == "ExtraInfo" && !string.IsNullOrEmpty(value)) {
extraInfo = value;
}
}
}
NavigationExtension.HandlePushNotificationNavigation(area, extraInfo);
}
使用OnNewIntent
来拦截用户与之交互时的推送通知。
MyFirebaseMessaging.cs
using System;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Support.V4.App;
using Android.Util;
using Firebase.Messaging;
using PushTesting.Models;
using WindowsAzure.Messaging;
namespace PushTesting.Droid.Services {
[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class OcsFirebaseMessaging : FirebaseMessagingService {
private const string NotificationChannelId = "1152";
private const string NotificationChannelName = "Push Notifications";
private const string NotificationChannelDescription = "Receive notifications";
private NotificationManager notificationManager;
public override void OnNewToken(string token) => SendTokenToAzure(token);
/// <summary>
/// Sends the token to Azure for registration against the device
/// </summary>
private void SendTokenToAzure(string token) {
try {
NotificationHub hub = new NotificationHub(Constants.AzureConstants.NotificationHub, Constants.AzureConstants.ListenConnectionString, Android.App.Application.Context);
Task.Run(() => hub.Register(token, new string[] { }));
} catch (Exception ex) {
Log.Error("ERROR", $"Error registering device: {ex.Message}");
}
}
/// <summary>
/// When the app receives a notification, this method is called
/// </summary>
public override void OnMessageReceived(RemoteMessage remoteMessage) {
bool hasTitle = remoteMessage.Data.TryGetValue("title", out string title);
bool hasBody = remoteMessage.Data.TryGetValue("body", out string body);
bool hasArea = remoteMessage.Data.TryGetValue("area", out string area);
bool hasExtraInfo = remoteMessage.Data.TryGetValue("extraInfo", out string extraInfo);
PushNotificationModel push = new PushNotificationModel {
Title = hasTitle ? title : string.Empty,
Body = hasBody ? body : string.Empty,
Area = hasArea ? area : string.Empty,
ExtraInfo = hasExtraInfo ? extraInfo : string.Empty
};
SendNotification(push);
}
/// <summary>
/// Handles the notification to ensure the Notification manager is updated to alert the user
/// </summary>
private void SendNotification(PushNotificationModel push) {
// Create relevant non-repeatable Id to allow multiple notifications to be displayed in the
<details>
<summary>英文:</summary>
I've seen **many** different types of solutions which may have worked in the past, but nothing solid that has worked for myself. And it's a minefield of what people say works, what doesn't work, what has changed, etc. But I'm trying to find not only a solution but hopefully an understanding - because right now I am seriously confused.
---
**What I can do right now** - My Xamarin Forms app (Android) can receive push notification if the app is in the Foreground/Background, I can also intercept these notifications when the user taps them so I can tell my app what to do.
**What I am trying to do** - Essentially the above but in the state of where the app has been completely stopped.
---
I have my Firebase Messaging setup which is wired to Azure Notification Hub - unfortunately, I won't be moving away from Azure (just in case anyone suggests to drop it). Most of what I have currently is information I've managed to stitch together from various Microsoft Documentation ([here][1] I don't use AppCenter - just used this to cross-reference any useful code, [here][2], [here][3], and [here][4] ), other StackOverflow questions (such as [here][5], [here][6], and [here][7] - far too many more to link) and the Xamarin forum - so again, apologies if there is any obsolete code being used (please let me know - I have tried my best to use up-to-date methods, etc).
The type of push notifications that I am sending are **Data Messages** [which I read up on here][8], I'm using custom data in my notifications therefore it is my understanding this is the correct type of push I want to send, as shown below.
{
"data": {
"title": "Title Test",
"body": "Push notification body test",
"area": "SelectedPage"
}
}
Below is the current code I have setup in my project to handle push notifications thus far.
**Manifest**
-
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.pushtesting" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="Push Testing">
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
**MainActivity.cs**
-
I have `LaunchMode = LaunchMode.SingleTop`, would my understanding be correct, for this is to ensure the current Activity is still used instead of creating a new one - for instance, when the user would tap a notification - implementing it plus additional code (below) seems to suggest this is true.
protected override void OnNewIntent(Intent intent) {
base.OnNewIntent(intent);
String area = String.Empty;
String extraInfo = String.Empty;
if (intent.Extras != null) {
foreach (String key in intent.Extras.KeySet()) {
String value = intent.Extras.GetString(key);
if (key == "Area" && !String.IsNullOrEmpty(value)) {
area = value;
} else if (key == "ExtraInfo" && !String.IsNullOrEmpty(value)) {
extraInfo = value;
}
}
}
NavigationExtension.HandlePushNotificationNavigation(area, extraInfo);
}
Using `OnNewIntent` to intercept the push notification when the user interacts with it.
**MyFirebaseMessaging.cs**
-
using System;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Support.V4.App;
using Android.Util;
using Firebase.Messaging;
using PushTesting.Models;
using WindowsAzure.Messaging;
namespace PushTesting.Droid.Services {
[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class OcsFirebaseMessaging : FirebaseMessagingService {
private const String NotificationChannelId = "1152";
private const String NotificationChannelName = "Push Notifications";
private const String NotificationChannelDescription = "Receive notifications";
private NotificationManager notificationManager;
public override void OnNewToken(String token) => SendTokenToAzure(token);
/// <summary>
/// Sends the token to Azure for registration against the device
/// </summary>
private void SendTokenToAzure(String token) {
try {
NotificationHub hub = new NotificationHub(Constants.AzureConstants.NotificationHub, Constants.AzureConstants.ListenConnectionString, Android.App.Application.Context);
Task.Run(() => hub.Register(token, new String[] { }));
} catch (Exception ex) {
Log.Error("ERROR", $"Error registering device: {ex.Message}");
}
}
/// <summary>
/// When the app receives a notification, this method is called
/// </summary>
public override void OnMessageReceived(RemoteMessage remoteMessage) {
Boolean hasTitle = remoteMessage.Data.TryGetValue("title", out String title);
Boolean hasBody = remoteMessage.Data.TryGetValue("body", out String body);
Boolean hasArea = remoteMessage.Data.TryGetValue("area", out String area);
Boolean hasExtraInfo = remoteMessage.Data.TryGetValue("extraInfo", out String extraInfo);
PushNotificationModel push = new PushNotificationModel {
Title = hasTitle ? title : String.Empty,
Body = hasBody ? body : String.Empty,
Area = hasArea ? area : String.Empty,
ExtraInfo = hasExtraInfo ? extraInfo : String.Empty
};
SendNotification(push);
}
/// <summary>
/// Handles the notification to ensure the Notification manager is updated to alert the user
/// </summary>
private void SendNotification(PushNotificationModel push) {
// Create relevant non-repeatable Id to allow multiple notifications to be displayed in the Notification Manager
Int32 notificationId = Int32.Parse(DateTime.Now.ToString("MMddHHmmsss"));
Intent intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
intent.PutExtra("Area", push.Area);
intent.PutExtra("ExtraInfo", push.ExtraInfo);
PendingIntent pendingIntent = PendingIntent.GetActivity(this, notificationId, intent, PendingIntentFlags.UpdateCurrent);
notificationManager = (NotificationManager)GetSystemService(Context.NotificationService);
// Creates Notification Channel for Android devices running Oreo (8.0.0) or later
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) {
NotificationChannel notificationChannel = new NotificationChannel(NotificationChannelId, NotificationChannelName, NotificationImportance.High) {
Description = NotificationChannelDescription
};
notificationManager.CreateNotificationChannel(notificationChannel);
}
// Builds notification for Notification Manager
Notification notification = new NotificationCompat.Builder(this, NotificationChannelId)
.SetSmallIcon(Resource.Drawable.ic_launcher)
.SetContentTitle(push.Title)
.SetContentText(push.Body)
.SetContentIntent(pendingIntent)
.SetAutoCancel(true)
.SetShowWhen(false)
.Build();
notificationManager.Notify(notificationId, notification);
}
}
}
Then finally my Firebase class where I use `OnNewToken()` to register to Azure with, `OnMessageReceived()` is overridden so I can handle a push notification being received and then `SendNotification()` which is being used to build and show the notification.
Any and all help will be highly appreciated!
*[Added Sample Project to GitHub][9]*
[1]: https://learn.microsoft.com/en-us/appcenter/sdk/push/xamarin-android
[2]: https://learn.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-xamarin-forms-get-started-push#configure-and-run-the-android-project-optional
[3]: https://learn.microsoft.com/en-us/azure/notification-hubs/xamarin-notification-hubs-push-notifications-android-gcm
[4]: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/data-cloud/azure-services/azure-notification-hub#register-templates-and-tags-with-the-azure-notification-hub
[5]: https://stackoverflow.com/questions/37711082/how-to-handle-notification-when-app-in-background-in-firebase/37845174#37845174
[6]: https://stackoverflow.com/questions/57824810/xamarin-forms-android-not-receiving-push-notifications-when-closed
[7]: https://stackoverflow.com/questions/48186996/firebase-push-notifications-not-working-properly-in-some-devices
[8]: https://firebase.google.com/docs/cloud-messaging/concept-options
[9]: https://github.com/MattVon/Xamarin-Push-Notification-Sample
</details>
# 答案1
**得分**: 1
以下是翻译好的部分:
- 对于我来说有一些问题(更多细节[在此][1]):
- 对Android应用程序架构以及与FirebaseMessagingService生命周期的关系不熟悉:有应用程序、服务和接收器。当“应用程序”(从用户角度看)被关闭,通过从最近使用中滑动时,服务/接收器可用于响应“意图”(传入的通知消息)。当应用程序被“强制停止”时,不会运行UI、服务或接收器。
- Visual Studio在调试会话结束后强制停止应用程序。要诊断“停止”的应用程序,您需要在设备上运行该应用程序并查看设备日志。
- FirebaseMessagingService的生命周期是这样的,当应用程序处于停止状态时,构造函数不再具有对共享应用程序的属性或方法的访问权限(我不得不通过删除抽象并使代码特定于平台来解决这个问题 - 特别是无法使用的DependencyService)。
- 微软的文档已经过时。例如,Firebase清单条目不再需要Receiver元素。
- 无法将调试器附加到“停止”的应用程序中的Visual Studio。
- 设备上的错误消息很难理解。
最终的解决方案是查看->其他窗口->设备日志,并再次运行应用程序以避免强制停止状态,以发现构造函数生命周期问题,我不得不将触及共享应用程序库的代码移到FirebaseMessagingService之外。
[1]: https://github.com/xamarin/GooglePlayServicesComponents/issues/273
<details>
<summary>英文:</summary>
There were a number of issues for me (more details [here][1]):
- Unfamiliarity with the Android app architecture and how that relates to the FirebaseMessagingService lifecycle: There is the app, services and receivers. When the 'app' (from a user perspective) is closed, by swiping from recents, the services/receivers are available to react to 'intents' (the incoming notification message). When the app is 'force stopped', no ui, services or receivers run.
- Visual Studio force stops apps after the debug session ends. To diagnose a 'stopped' app you need to run the app on device and look at Device Logs.
- The FirebaseMessagingService lifecycle is such that when the app is in the stopped state the ctor no longer has access to properties or methods of the shared app (I had to work around this by removing abstractions and making code platform specific - in particular a DependencyService that could not be used as it was not available).
- MS docs are out of date. Firebase manifest entries no longer require the Receiver elements for example.
- You cannot attach debugger to a 'stopped' app in Visual Studio.
- Error messages on device are cryptic.
In the end the solution was to View->Other Windows -> Device Logs and run the app a second time to avoid the Force Stopped state, to discover the ctor lifecycle issue, which I had to work around by moving out code that touched the shared app library from FirebaseMessagingService.
[1]: https://github.com/xamarin/GooglePlayServicesComponents/issues/273
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论