Google cloud message 'Not Registered' fail

2019-01-19 06:48发布

I'm developing an Android app using Xamarin Forms that's primary purpose is receiving push notifications of events. I've had some seemingly random trouble receiving Not Registered failures when sending a notification, after the device successfully calls GcmPubSub.getInstance().subscribe(). This was happening a week or 2 ago and I thought I had the problem resolved by always using the main application context for token generation and the getInstance() call.

Yesterday around noon EST the problem reoccurred and then just as suddenly started working around 4:00 - 4:30. Afternoon was full of commenting code to simplify things and other random things like removing and re-adding NuGet packages. Now I'm back to the code that I had in place just before it stopped working yesterday and everything is happy as a clam.

When this problem happens it's only when the subscribe() call is made over wifi. If I debug the app on my phone on the cellular network I never receive the Not Registered failure.

I'm currently calling unsubscribe() when the user logs off in the app, and I've been able to unsubscribe and re-subscribe successfully (this morning).

Is unsubscribing on log off a best practice for push notifications when the notifications are user specific? I thought that there may be a possibility that this was making the GCM servers confused in some way.

Any suggestions on why I might be receiving the Not Registered failures would be awesome too.

The registration (subscribe/unsubscribe) services:

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

The 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>

The main activity:

[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;
        }
    }

}

The server side sending of a notification. The Device.CloudMessageToken is populated by the DeviceService.UpdateCloudMessageToken (token) call in the registration service above:

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条回答
男人必须洒脱
2楼-- · 2019-01-19 07:31

So yes, the changing of the token seems to have truly fixed my issue. As I was testing the new logic I had a scenario where a token that the InstanceId wanted to use what returning as Not Registered. After I deleted the InstanceId and re-generated a new token I was successfully able to send a message to the device.

As a side note, I also removed the unsubscribe() call from the logout logic. Thanks for the link @gerardnimo

To accomplish this I created a new service which deletes the token and the InstanceId (though I probably only need to delete the InstanceId), and then calls the 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);

    }
}
查看更多
劳资没心,怎么记你
3楼-- · 2019-01-19 07:32

I encountered the same Xamarin issue myself, apparently when you deploy using Xamarin Studio it somehow replaces the APK without triggering whatever is necessary for GetToken() to produce a new token.

The proper solution would be to detect if a Xamarin Studio deployment has occurred and use DeleteInstanceID() to force a token refresh.

The closest I came up with was to detect if the APK has been replaced (either via regular update or Xamarin Studio deployment) and force token refresh only in those cases.

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;
}

and the main GcmRegistrationService method:

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;
    }
}
查看更多
登录 后发表回答