谷歌云消息“未注册”失败并取消订阅最佳做法?

Posted

技术标签:

【中文标题】谷歌云消息“未注册”失败并取消订阅最佳做法?【英文标题】:Google cloud message 'Not Registered' failure and unsubscribe best practices? 【发布时间】:2016-04-03 11:43:59 【问题描述】:

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

昨天美国东部标准时间中午左右,问题再次出现,然后在 4:00 - 4:30 左右突然开始工作。下午充满了注释代码以简化事情和其他随机事情,例如删除和重新添加 NuGet 包。现在我回到昨天停止工作之前的代码,一切都像蛤蜊一样快乐。

当这个问题发生时,只有当subscribe() 呼叫是通过wifi 进行时。如果我在蜂窝网络上调试手机上的应用程序,我永远不会收到Not Registered 故障。

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

当通知是特定于用户的推送通知时,取消订阅注销是否是推送通知的最佳做法?我认为这可能会在某种程度上使 GCM 服务器感到困惑。 p>

任何关于为什么我可能会收到 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;
        
    


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

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());
        

    


【问题讨论】:

这可能与this 重复。 GcmRegistrationService.GetIntent(this, NotificationTopic) 可能指向与 InstanceID 和 GcmPubSub 不同的上下文。 很好,尽管几周前我经历了这个,发现我需要使用主应用程序上下文。抱歉,我应该更明确地引用它。 本文档可能会为正确的unsubscription 进程提供一些启示。 下次发生这种情况(如果再次发生)我将尝试显式卸载。我很确定我在昨天的实验中这样做了,但听起来开发部署可能会触发类似卸载的场景,其中 GCM 服务器正在标记要删除的令牌 再想一想,我将跟踪我在服务器上收到Not Registered 响应的令牌,如果再次尝试使用该令牌,我将尝试响应-新令牌消息。我们会看到它是如何飞行的 【参考方案1】:

所以是的,令牌的更改似乎确实解决了我的问题。当我测试新逻辑时,我遇到了一个场景,其中 InstanceId 想要使用的令牌返回为Not Registered。在我删除了 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 调试问题!?这会在生产中发生吗!? 我认为这只是一个调试问题,在部署到 VM 或设备时似乎被“卸载”(至少这是我的怀疑) 是的,经过进一步测试,当您更改 AndroidManifest 并部署时会发生这种情况。以前的应用程序已“卸载”并且令牌无效。我在生产中进行了测试,在那里我推出了更新以查看旧版本是否也被“卸载”,但事实并非如此。仅在 Xamarin 调试期间发生。经过这么多问题,我们正式放弃了 Xamarin,它们弊大于利,也浪费时间。 似乎是我的问题,在部署之间卸载并重新安装应用程序!在我的情况下,我在尝试订阅主题时收到了 INVALID_PARAMETERS(虽然是间歇性的)【参考方案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;
    

【讨论】:

以上是关于谷歌云消息“未注册”失败并取消订阅最佳做法?的主要内容,如果未能解决你的问题,请参考以下文章

gcm推送通知:先成功,后IOS未注册

Kubernetes pod 在使用未知证书授权调用谷歌云发布/订阅时失败

谷歌云发布订阅延迟消息

谷歌云平台推送订阅发送重复消息 ID 字段

如何取消删除谷歌云功能

谷歌云消息安全