There may be times where we create an app that needs to receive messages or events from a remote server. Instead of polling the server for updates, we want the server to let our app know directly.
Push notifications allow a remote server to send a message directly to a specific device. To implement push notifications, we will need a project number and an API key from the Google Developers Console (https://cloud.google.com/console):
0.0.0.0/0
—and click on Create:After we have obtained the required keys, we can start implementing the client. The client registers with the cloud service and then is able to receive push notifications:
[assembly: UsesPermission(Manifest.Permission.Internet)] [assembly: UsesPermission(Manifest.Permission.GetAccounts)] [assembly: UsesPermission(Manifest.Permission.WakeLock)] [assembly: UsesPermission( "com.google.android.c2dm.permission.RECEIVE")]
.permission.C2D_MESSAGE
appended. Instead of hardcoding the package name, we use the @PACKAGE_NAME@
value:[assembly: UsesPermission( "@[email protected]_MESSAGE")]
private bool CheckPlayServices() { var play = GoogleApiAvailability.Instance; var result = play.IsGooglePlayServicesAvailable(this); if (result == ConnectionResult.Success) return true; if (play.IsUserResolvableError(result)) { play.GetErrorDialog(this, result, 0)).Show(); } else { Finish(); } return false; }
private Task<string> GetRegistrationAsync() { return Task.Run(() => { string token = null; try { var instanceId = InstanceID.GetInstance(this); token = instanceId.GetToken( "PROJECT_NUMBER", GoogleCloudMessaging.InstanceIdScope); // send the token to the server that sends messages } catch (Exception ex) { // handle/log the error and let the user try again } return token; }); }
OnCreate()
method:if (CheckPlayServices()) { registrationId = await GetRegistrationAsync(); }
OnResume
:CheckPlayServices();
Now that we have the connection set up and the device registered with the cloud service, we need to get the receiver in place so that we can actually receive incoming messages:
GcmReceiver
instance that will receive incoming push notifications. It doesn't do anything except allow us to attach attributes:[BroadcastReceiver( Permission = "com.google.android.c2dm.permission.SEND", Exported = true)] [IntentFilter( new []{ "com.google.android.c2dm.intent.RECEIVE", "com.google.android.c2dm.intent.REGISTRATION" }, Categories = new[]{ "@PACKAGE_NAME@" })] public class NotificationReceiver : GcmReceiver { }
GcmListenerService
and has a specific intent filter. We can then process the message in the OnMessageReceived()
method:[Service] [IntentFilter(new []{ "com.google.android.c2dm.intent.RECEIVE })] public class NotificationService : GcmListenerService { public override void OnMessageReceived( string from, Bundle data) { // process the data bundle var message = data.GetString("cookbook_message"); } }
Sometimes the token becomes invalid, for example if the server or Google decide to refresh the tokens. When this happens, our app will be notified:
InstanceIDListenerService
type:[Service] [IntentFilter( new []{ "com.google.android.gms.iid.InstanceID" })] public class InstanceIdService : InstanceIDListenerService { public override void OnTokenRefresh() { var instanceId = InstanceID.GetInstance(this); var token = instanceId.GetToken( "PROJECT_NUMBER", GoogleCloudMessaging.InstanceIdScope); } }
In order for a device to receive messages, they have to be sent from our server to the cloud service. There are several ways to do this, but here we use HTTP requests:
var server = "https://android.googleapis.com/gcm/send"; var message = string.Format( "{{" + " "registration_ids": [ "{0}" ]" + " "data": {{" + " "cookbook_message": "Hello World!"" + " }}" + "}}", registrationId); var bytes = Encoding.UTF8.GetBytes(message); var request = WebRequest.CreateHttp(server); request.Method = "POST"; request.ContentType = "application/json"; request.ContentLength = bytes.Length; var stream = await request.GetRequestStreamAsync(); stream.Write(bytes, 0, bytes.Length); stream.Close();
request.Headers.Add( HttpRequestHeader.Authorization, "key=API_KEY"); await request.GetResponseAsync();
Push notifications are very useful for many types of apps. They allow a remote server to send a message directly to the phone and our app, without the app having to continually poll the server. The app registers with GCM and waits for the cloud service to send messages. Once a message is received, the service broadcasts the message, which the app can then handle.
A server that wants to send a message to a device actually sends the message to GCM but specifies which device should receive it. Once GCM receives the message, it is relayed to the actual device.
The advantage of using GCM is that our app does not have to query the server or maintain connections to the server. If each app tried to connect to servers just to check if there was a message, the battery would drain quickly. Using a centralized connection, the service can open and maintain connections instead of each app. This also reduces much of the code that would be needed to maintain connections to a server as well as actually maintaining an actual server for the device to connect to.
As there is a lot of work going on underneath, we have to include the Google Play Services library to do the work for us. This library contains all the logic needed to interact with GCM on an Android device.
Along with this library, we have to request several permissions for the app. First, we will need the Internet
and GetAccounts
permissions. Because we are going to use GCM to receive messages, we need the com.google.android.c2dm.permission.RECEIVE
permission. As the device may be asleep when a message comes through, we need the WakeLock
permission so that we can wake the device to process the incoming message.
Finally, we need to request an app-specific permission. This permission is the package name appended with .permission.C2D_MESSAGE
. Instead of hardcoding the package name, we make use of the @PACKAGE_NAME@
value. This value is then replaced at compile time with the actual package name.
Before we can interact with GCM, we have to first make sure that Google Play Services is installed on the device. This is a separate package from the Play Store that we can ask the user to install. First, we make sure that Google Play Services is installed by invoking the IsGooglePlayServicesAvailable()
method on the GoogleApiAvailability
instance. The result is a description of the error. If everything is in place, the result will be ConnectionResult.Success
. Sometimes there is an issue that the user can fix, such as when the services are disabled or not installed. We check for this using the IsUserResolvableError()
method. If this method returns true
, we can display a dialog that will allow the user to rectify the problem. We get the dialog as a result of the GetErrorDialog()
method. If there is any other problem, we can either close the app or allow the user to use the app without GCM, depending on what is required or supported.
If there are no errors and everything is installed, we can register with GCM and obtain an instance token that identifies the instance of the app on a device. To do this, we first need an instance of the InstanceID
type, which we obtain by invoking the GetInstance()
method. With the result, we can invoke the GetToken()
method. This method requires that we have a valid Project Number, and, because we are going to be using the messaging APIs; we specify the GoogleCloudMessaging.InstanceIdScope
method as the scope. The result of this method is a string instance token for the device. The Project Number is obtained from the Google Developers Console. As this is an input and output operation across a network, we must run this on a background thread.
Once we have registered, we need to send the token to whatever server will be sending notifications to our app. In the case of this example, the server will be the device. Our device will now receive any messages from the GCM server. However, in order to be able to handle these messages, we have to create a broadcast receiver. Because the device may be sleeping when a message is received, we use a GcmReceiver
instance to ensure that the message is handled before going back to sleep.
The receiver is usually very simple and just passes the incoming intent on to a service. But in order for the receiver to receive messages, we have to specify what messages we want to receive. We use the [BroadcastReceiver]
attribute to specify that this receiver only accepts broadcasts from GCM services. To do this, we set the Permission
property to com.google.android.c2dm.permission.SEND
. Finally, we have to let the system know we are only interested in messages sent to this app from the GCM server. In order to ignore other messages, we specify an [IntentFilter]
attribute with the action set to com.google.android.c2dm.permission.SEND
and the Categories
property set to the app package name.
Once a message is received, it will get redirected to a service that has the com.google.android.c2dm.intent.RECEIVE
intent filter. The easiest way to handle these messages is to create a service that inherits from the GcmListenerService
type. We can read the message using the bundle provided to the OnMessageReceived()
method. How the message is processed depends on how the message was structured when it was sent.
The InstanceID
type is used to get a token when implementing push notifications, but this token may change over time, although often not frequently. Tokens may be reset, and when this happens, our app must request a new token. By creating a service that inherits from InstanceIDListenerService
and has the com.google.android.gms.iid.InstanceID
intent filter, we can handle this.
One of the ways to send a message to the GCM server is to use the HTTP server located at the address https://android.googleapis.com/gcm/send. We can send a JSON message to this server using an HTTP POST request:
{ "registration_ids": ["DEVICE_REGISTRATION_ID"], "data": { "JSON-KEY": "JSON-VALUE" } }
The HTTP message needs to be authorized with the API key from the Google Developers Console. The key=API_KEY
value is used in the authorization header. This is because all servers will send messages to the same server, so they are distinguished using the authorization header.
Push notifications can be used to reduce the number of times an app needs to communicate with a server. If all messages are sent directly to the device, there will be no need to check for messages on the server. However, if the device goes offline, for example if it is turned off, there is a limit to how many messages will be stored. Currently, the limit is 100 messages. If the limit is reached, all the stored messages are discarded and replaced with a special message that indicates a full sync is needed.
It is important to remember that only a tiny amount of data can be sent via push. Currently, the maximum message size is 4 KB. To send larger messages, we can send a message that indicates that the app should sync its data.