谷歌云消息推送“未注册”故障和取消订阅的最佳实践?

4
我正在使用Xamarin Forms开发一个Android应用程序,主要目的是接收事件推送通知。当发送通知时,设备成功调用GcmPubSub.getInstance().subscribe()后,我遇到了一些看似随机的Not Registered故障。这个问题在一两周前就出现了,我认为通过始终使用主应用程序上下文进行令牌生成和getInstance()调用来解决问题。

昨天中午12点左右,问题再次发生,然后突然在4:00 - 4:30之间开始工作。下午充满了注释代码以简化事情以及其他随机事情,例如删除和重新添加NuGet包。现在我回到了昨天停止工作之前放置的代码,并且一切都很愉快。

当此问题发生时,仅在通过wifi进行subscribe()调用时才会出现。如果我在手机上使用蜂窝网络调试应用程序,则永远不会收到Not Registered失败消息。

当前我正在应用程序中的用户注销时调用unsubscribe(),并且我已经成功取消订阅并重新订阅(今天早上)。

当通知是特定于用户时,注销时是否最佳实践推送通知?我认为可能会导致GCM服务器以某种方式混淆。

您对我可能收到Not Registered失败消息的原因有任何建议也将是令人敬畏的。

注册(订阅/取消订阅)服务:

namespace MyApp.Droid.Services
{
    /// <summary>
    /// The background process that handles retrieving GCM token
    /// </summary>
    [Service(Exported = true)]
    public class GcmRegistrationService : IntentService
    {
        private static readonly object Locker = new object();

        public GcmRegistrationService() : base("GcmRegistrationService") { }

        public static Intent GetIntent(Context context, string topic)
        {
            var valuesForActivity = new Bundle();
            valuesForActivity.PutString("topic", topic);

            var intent = new Intent(context, typeof(GcmRegistrationService));

            intent.PutExtras(valuesForActivity);

            return intent;
        }

        protected override async void OnHandleIntent(Intent intent)
        {
            try
            {
                // Get the count value passed to us from MainActivity:
                var topic = intent.Extras.GetString("topic", "");

                if (string.IsNullOrWhiteSpace(topic))
                    throw new Java.Lang.Exception("Missing topic value");

                string token;

                Log.Info("RegistrationIntentService", "Calling InstanceID.GetToken");
                lock (Locker)
                {
                    var instanceId = InstanceID.GetInstance(Forms.Context);
                    var projectNumber = Resources.GetString(Resource.String.ProjectNumber);
                    token = instanceId.GetToken(projectNumber, GoogleCloudMessaging.InstanceIdScope, null);

                    Log.Info("RegistrationIntentService", "GCM Registration Token: " + token);

                    Subscribe(token, topic);
                }

                var applicationState = ApplicationStateService.GetApplicationState ();

                // Save the token to the server if the user is logged in
                if(applicationState.IsAuthenticated)
                    await SendRegistrationToAppServer(token);
            }
            catch (SecurityException e)
            {
                Log.Debug("RegistrationIntentService", "Failed to get a registration token because of a security exception");
                Log.Debug ("RegistrationIntentService", "Exception message: " + e.Message);
                //ToastHelper.ShowStatus("Google Cloud Messaging Security Error");
                throw;
            }
            catch (Java.Lang.Exception e)
            {
                Log.Debug("RegistrationIntentService", "Failed to get a registration token");
                Log.Debug ("RegistrationIntentService", "Exception message: " + e.Message);
                //ToastHelper.ShowStatus("Google Cloud Messaging Error");
                throw;
            }

        }

        private async System.Threading.Tasks.Task SendRegistrationToAppServer(string token)
        {
            // Save the Auth Token on the server so messages can be pushed to the device
            await DeviceService.UpdateCloudMessageToken (token);

        }

        void Subscribe(string token, string topic)
        {

            var pubSub = GcmPubSub.GetInstance(Forms.Context);

            pubSub.Subscribe(token, "/topics/" + topic, null);
            Log.Debug("RegistrationIntentService", "Successfully subscribed to /topics/" +topic);
            ApplicationStateService.SaveCloudMessageToken(token, topic);
        }

    }


    /// <summary>
    /// The background process that handles unsubscribing GCM token
    /// </summary>
    [Service(Exported = false)]
    public class GcmUnsubscribeService : IntentService
    {

        private static readonly object Locker = new object();

        public GcmUnsubscribeService() : base("GcmUnsubscribeService") { }

        public static Intent GetIntent(Context context, ApplicationState applicationState, bool resubscribe=false)
        {
            var valuesForActivity = new Bundle();

            valuesForActivity.PutString ("token", applicationState.CloudMessageToken);
            valuesForActivity.PutString ("topic", applicationState.Topic);
            valuesForActivity.PutBoolean ("resubscribe", resubscribe);

            var intent = new Intent(context, typeof(GcmUnsubscribeService));

            intent.PutExtras(valuesForActivity);

            return intent;
        }

        protected override void OnHandleIntent(Intent intent)
        {

            // Get the count value passed to us from MainActivity:
            var token = intent.Extras.GetString("token", "");
            var topic = intent.Extras.GetString("topic", "");
            var resubscribe = intent.Extras.GetBoolean ("resubscribe");

            var pubSub = GcmPubSub.GetInstance(Forms.Context);
            try
            {
                pubSub.Unsubscribe (token, "/topics/" + topic);
            }
            catch(IOException e) 
            {
                var x = e.Message;
            }

            if (resubscribe) {
                var subscribeIntent = GcmRegistrationService.GetIntent(Forms.Context, topic);
                Forms.Context.StartService(subscribeIntent);
            }
        }
    }
}

AndroidManifest.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:installLocation="auto" 
    package="com.me.notification_app" 
    android:versionCode="1" 
    android:versionName="1.0">

    <uses-sdk android:minSdkVersion="19" />

    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <permission 
        android:name="com.me.notification_app.permission.C2D_MESSAGE" 
        android:protectionLevel="signature" />

    <uses-permission 
        android:name="com.me.notification_app.permission.C2D_MESSAGE" />

    <application 
        android:label="Notification App" 
        android:icon="@drawable/icon">

        <receiver 
            android:name="com.google.android.gms.gcm.GcmReceiver" 
            android:permission="com.google.android.c2dm.permission.SEND"
            android:exported="true">

            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="com.me.notification_app" />
            </intent-filter>

        </receiver>

    </application>
</manifest>

主要活动:
[Activity(Label = "MyApp", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
    public static string NotificationTopic = "MyEvent";

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App(DeviceType.Android));

        if (IsPlayServicesAvailable())
        {
            var intent = GcmRegistrationService.GetIntent(this, NotificationTopic);
            StartService(intent);
        }
    }


    public bool IsPlayServicesAvailable()
    {
        var resultCode = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(this);
        if (resultCode != ConnectionResult.Success)
        {
            if (GoogleApiAvailability.Instance.IsUserResolvableError(resultCode))
                ToastHelper.ShowStatus("Google Play Services error: " + GoogleApiAvailability.Instance.GetErrorString(resultCode));
            else
            {
                ToastHelper.ShowStatus("Sorry, notifications are not supported");
            }
            return false;
        }
        else
        {                
            return true;
        }
    }

}

服务器端发送通知。在上面的注册服务中,DeviceService.UpdateCloudMessageToken(token) 调用会填充 Device.CloudMessageToken

public async Task SendNotificationAsync(Device device, string message, Dictionary<string, string> extraData = null)
{
    if (string.IsNullOrWhiteSpace(device.CloudMessageToken))
        throw new Exception("Device is missing a CloudMessageToken");

    var apiKey = _appSettingsHelper.GetValue("GoogleApiKey");
    var gcmBaseUrl = _appSettingsHelper.GetValue("GoogleCloudMessageBaseUrl");
    var gcmSendPath = _appSettingsHelper.GetValue("GoogleCloudMessageSendPath");

    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri(gcmBaseUrl);
        client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=" + apiKey);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));


        var messageInfo = new MessageInfo
        {
            to = device.CloudMessageToken,
            data = new Dictionary<string, string>
            {
                {"message", message}
            }
        };

        if (extraData != null)
        {
            foreach (var data in extraData)
            {
                messageInfo.data.Add(data.Key, data.Value);
            }
        }

        var messageInfoJson = JsonConvert.SerializeObject(messageInfo);

        var response =
            await
                client.PostAsync(gcmSendPath,
                    new StringContent(messageInfoJson, Encoding.UTF8, "application/json"));

        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync();

        var contentValues = JsonConvert.DeserializeObject<Dictionary<string, object>>(content);

        if ((long)contentValues["failure"] == 1)
        {
            var results = (JArray)contentValues["results"];
            throw new Exception(results[0]["error"].ToString());
        }

    }

}

2
这可能是 的重复。 GcmRegistrationService.GetIntent(this, NotificationTopic) 可能指向与 InstanceID 和 GcmPubSub 不同的上下文。 - gerardnimo
1
不错的发现,尽管我在几周前经历了这个问题并发现我需要使用主应用程序上下文。我很抱歉,我应该更明确地引用它。 - Josh Russo
1
这份文档可能会为正确的取消订阅过程提供一些帮助。 - gerardnimo
1
下次如果这种情况再次发生,我将尝试进行显式卸载。我相当确定昨天在我的实验中已经这样做了,但似乎开发部署可能会触发类似卸载的情况,其中GCM服务器正在标记令牌以进行删除。 - Josh Russo
转念一想,我将跟踪在服务器上收到“未注册”响应的令牌,如果再次尝试使用该令牌,我将回复一个尝试新令牌的消息。我们看看这个方案是否可行。 - Josh Russo
2个回答

4

是的,更改令牌似乎真正解决了我的问题。在测试新逻辑时,我遇到了一种情况,即InstanceId想要使用的令牌返回为未注册。在我删除了InstanceId并重新生成了一个新的令牌后,我成功地能够向设备发送消息。

另外,我还从注销逻辑中删除了unsubscribe()调用。感谢@gerardnimo提供的链接。

为了实现这个目标,我创建了一个新的服务,它删除了令牌和InstanceId(虽然我可能只需要删除InstanceId),然后调用了GcmRegistrationService

/// <summary>
/// Gcm reregistration service to delete and recreate the token.
/// </summary>
[Service(Exported = false)]
public class GcmReregistrationService : IntentService
{

    private static readonly object Locker = new object();

    public GcmReregistrationService() : base("GcmReregistrationService") { }

    public static Intent GetIntent(Context context, string token, string topic)
    {
        var valuesForActivity = new Bundle();

        valuesForActivity.PutString ("token", token);
        valuesForActivity.PutString ("topic", topic);

        var intent = new Intent(context, typeof(GcmReregistrationService));

        intent.PutExtras(valuesForActivity);

        return intent;
    }

    protected override void OnHandleIntent(Intent intent)
    {

        // Get the count value passed to us from MainActivity:
        var token = intent.Extras.GetString("token", "");
        var topic = intent.Extras.GetString("topic", "");

        var instanceId = InstanceID.GetInstance(Forms.Context);
        instanceId.DeleteToken (token, GoogleCloudMessaging.InstanceIdScope);
        instanceId.DeleteInstanceID ();

        var subscribeIntent = GcmRegistrationService.GetIntent(Forms.Context, topic);
        Forms.Context.StartService(subscribeIntent);

    }
}

我也遇到了同样的问题。删除instanceID/DeleteToken对我也起作用了。这是Xamarin调试问题吗?这会在生产中发生吗? - NinjaCowgirl
我相信这只是一个调试问题,当部署到虚拟机或设备时似乎出现了“卸载”的情况(至少这是我怀疑的)。 - Josh Russo
2
经过进一步测试,当您更改AndroidManifest并部署时,会发生这种情况。先前的应用程序被“卸载”,令牌也无效了。我在生产环境中进行了测试,推出了一个更新,以查看旧版本是否也被“卸载”,但事实并非如此。只有在Xamarin调试期间才会发生。经历了这么多问题后,我们已正式放弃Xamarin,他们所做的事情比好处还要多,而且浪费时间。 - NinjaCowgirl
似乎是我的问题,在部署之间卸载并重新安装应用程序!在我的情况下,当尝试订阅主题时(虽然不定期),我会收到INVALID_PARAMETERS错误。 - Rippo

2

我自己也遇到了同样的Xamarin问题,很明显在使用Xamarin Studio部署时,它会替换APK而不触发GetToken()生成新令牌所需的任何操作。

正确的解决方法是检测是否发生了Xamarin Studio部署,并使用DeleteInstanceID()强制刷新令牌。

我能想到的最接近的解决方案是检测APK是否已被替换(通过常规更新或Xamarin Studio部署),并仅在这些情况下强制刷新令牌。

private bool IsForcedTokenRefreshNeeded()
{
    DateTime actualWriteDT = GetAPKLastWriteDT();
    DateTime? storedLastWriteDT = RetrieveAPKLastWriteDT();
    bool forceTokenRefresh = false;
    if (storedLastWriteDT.HasValue)
    {
        if (actualWriteDT != storedLastWriteDT)
        {
            forceTokenRefresh = true;
            StoreAPKLastWriteDT(actualWriteDT);
        }
    }
    else
    {
        StoreAPKLastWriteDT(actualWriteDT); 
    }
    return forceTokenRefresh;
}

private void StoreAPKLastWriteDT(DateTime lastWriteDT)
{
    var prefs = Application.Context.GetSharedPreferences("MyApp", FileCreationMode.Private);
    var prefEditor = prefs.Edit();
    prefEditor.PutLong("APKLastModified", lastWriteDT.Ticks);
    prefEditor.Commit();
}

private DateTime? RetrieveAPKLastWriteDT()
{
    //retreive 
    var prefs = Application.Context.GetSharedPreferences("MyApp", FileCreationMode.Private);              
    long value = prefs.GetLong("APKLastModified", 0);
    if (value == 0)
    {
        return null;
    }
    return new DateTime(value);
}

private DateTime GetAPKLastWriteDT()
{
    string packageName = Android.App.Application.Context.PackageName;
    Android.Content.PM.ApplicationInfo appInfo = this.PackageManager.GetApplicationInfo(packageName, 0);
    string appFile = appInfo.SourceDir;
    return new FileInfo(appFile).LastWriteTimeUtc;
}

主要的GcmRegistrationService方法:

protected override void OnHandleIntent (Intent intent)
{
    Log.Info("GcmRegistrationService", "Calling InstanceID.GetToken");
    string token;
    bool forceTokenRefresh = IsForcedTokenRefreshNeeded();
    try
    {
        lock (m_lock)
        {
            InstanceID instanceID = InstanceID.GetInstance (Android.App.Application.Context);
            if (forceTokenRefresh)
            {
                Log.Info("GcmRegistrationService", "Forced token refresh");
                instanceID.DeleteInstanceID();
            }
            token = instanceID.GetToken(SenderID, GoogleCloudMessaging.InstanceIdScope, null);
            Log.Info("GcmRegistrationService", "GCM Registration Token: " + token);
        }
    }
    catch (Exception ex)
    {
        Log.Debug("GcmRegistrationService", "Failed to get a registration token: " + ex.Message);
        return;
    }

    try
    {
        SendRegistrationToAppServer(token);
    }
    catch(WebException)
    {
        if (forceTokenRefresh)
        {
            // this will force a refresh next time
            StoreAPKLastWriteDT(DateTime.MinValue);
        }
    }

    try
    {
        Subscribe(token);
    }
    catch (Exception ex)
    {
        Log.Debug("GcmRegistrationService", "Failed to subscribe: " + ex.Message);
        return;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接