Push notifications

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.

Getting ready

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):

  1. First, we have to set up a project on the Google Developers Console if we do not have one already. When logged in, click on Create Project and enter a project name and ID:
    Getting ready

    The Create Project dialog

  2. Once we have created or selected an existing project, we need to make a note of the Project Number. This is used to be able to receive messages:
    Getting ready

    The Project Number

  3. Select the APIs option under the APIs & auth section in the left-hand menu and make sure that the Google Cloud Messaging for Android option is on:
    Getting ready

    Enabling Google Cloud Messaging for Android

  4. Then, select the Credentials option under the APIs & auth section from the left-hand menu. In the Public API access section, click on Create New Key and then on Server Key:
    Getting ready

    Creating a new server key

  5. Enter the server's IP address—for testing we will use 0.0.0.0/0—and click on Create:
    Getting ready

    Entering the servers IP address

  6. On the new page, under the Public API access section, make a note of the API key. This is used to send messages with Google Cloud Messaging:
    Getting ready

    The API key

How to do it...

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:

  1. We will need to ensure that the Xamarin Google Play Services - Google Cloud Messaging (GCM) NuGet or component is installed into the project. This library contains the code needed to interact with Google Cloud Messaging.
  2. We will need several permissions in order to use 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")]
  3. There is also a special permission that we have to add, and this is the app package name with .permission.C2D_MESSAGE appended. Instead of hardcoding the package name, we use the @PACKAGE_NAME@ value:
    [assembly: UsesPermission(
      "@[email protected]_MESSAGE")]
  4. There are several things we need. The first is to make sure that Google Play Services is available on the device:
    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;
    }
  5. Next, using the project number from the Google Developers Console, we get our token from Google Play Services. Once we have obtained a token, we might want to send it to a server that will be sending messages to our app:
    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;
      });
    }
  6. When we load our activity, we can check for Google Play Services and then load the registration in the OnCreate() method:
    if (CheckPlayServices()) {
      registrationId = await GetRegistrationAsync();
    }
  7. To make sure that the app doesn't launch if Google Play Services is not installed, we can request the user installs it each time the app is resumed in 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:

  1. We create a 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 {
    }
  2. The receiver will redirect any messages to a service for processing. The service inherits from 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:

  1. To handle token refresh events, we create an interface that inherits from the 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:

  1. We can send a message to the cloud service using JSON, specifying a message and a device registration token:
    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();
  2. We have to authorize the request using the API key we got from the Google Developers Console:
    request.Headers.Add(
      HttpRequestHeader.Authorization, "key=API_KEY");
    await request.GetResponseAsync();

How it works...

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.

Note

Sending messages to a device is not done directly but rather through GCM.

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.

Note

Using GCM reduces the number of open connections, thus reducing battery usage.

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.

Note

The Google Play Services package needs to be installed from the store before push notifications can be used. However, on most devices it is already installed.

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.

Tip

Registration for push notifications must happen on a background thread as the method does not return immediately.

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.

Tip

The device may be asleep when a push notification is received.

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.

Note

Specific permissions and filters are used on the receiver to ensure that the app can and only will receive messages sent to that specific app.

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.

There's more...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset