I am developing an Android application using Xamarin Forms, in which the main goal is to push event notifications. I had some seemingly random problem with receiving failures Not Registeredwhen sending a notification after a successful device call GcmPubSub.getInstance().subscribe(). This happened a week or 2 ago, and I decided that the problem was solved, always using the main application context to generate a token and call getInstance().
Yesterday around noon EST, the problem recurred, and then just as suddenly started working around 4:00 - 4:30. The afternoon was full of code comments to simplify things and other random things like removing and re-adding NuGet packages. Now I returned to the code that was in my place before it stopped working yesterday, and everything was happy, like a clam.
When this problem occurs, only when the call subscribe()is made via Wi-Fi. If I debug an application on my phone on a cellular network, I never get a crash Not Registered.
I am currently invoking unsubscribe()when a user exits the application in the application and I was able to unsubscribe and re-subscribe successfully (this morning).
Disable disconnect on transition from best practice for push notifications when notifications are user-specific? I thought it might be possible that the GCM servers were somehow confused.
Any suggestions as to why I might get setbacks Not Registeredwould also be awesome.
Registration services (subscription / unsubscribe):
namespace MyApp.Droid.Services
{
[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
{
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 ();
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);
throw;
}
catch (Java.Lang.Exception e)
{
Log.Debug("RegistrationIntentService", "Failed to get a registration token");
Log.Debug ("RegistrationIntentService", "Exception message: " + e.Message);
throw;
}
}
private async System.Threading.Tasks.Task SendRegistrationToAppServer(string token)
{
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);
}
}
[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)
{
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>
Primary 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;
}
}
}
Sending notifications on the server side. Device.CloudMessageTokenpopulated by a call DeviceService.UpdateCloudMessageToken (token)to 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());
}
}
}