Skip to content

Instantly share code, notes, and snippets.

@justjanne
Last active August 19, 2017 01:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justjanne/153bd4886f92be0ee57fb408427c8d8b to your computer and use it in GitHub Desktop.
Save justjanne/153bd4886f92be0ee57fb408427c8d8b to your computer and use it in GitHub Desktop.

Cloud Messaging

This document contains all the required details to build a library able to subscribe and unsubscribe to cloud messaging services, and to build a receiver that handles received messages.

General Concept

RxJava is used to allow Promises for making calls.

The Api has two methods, subscribe and unsubscribe, each take a String for a scope and a subscription, and return an Observable.

The result of subscribe would be the token, of unsubscribe would be the application id that has been unsubscribed from.

Interface

public interface CloudMessagingApi {
    public Observable<String> subscribe(String scope, String sender, String subscription, String subtype);
    public Observable<String> unsubscribe(String scope, String sender, String subscription, String subtype);
}

Internal structure

The Api takes two config objects, one to contain versionCode, versionName and packageName of the current app, the other to contain the appId, sender, and clientVersion for the Service.

The Api has a Keymanager, which handles the creation and storage of an RSA or ECDSA keypair, and also handles signing messages.

A second helper is used to resolve the package name of the Google Play Services package, and its version code.

Interacting with the Api

The Api is configured with the config objects and the application context, and creates the helpers it requires itself.

Upon making a call, it creates the object representing the command, adds the required authentication key/value pairs, generates a unique id, and returns a PublishSubject that is also stored in a map by its id.

If a response to that call is received, the PublishSubject is looked up by its id and completed with the processed response to the call. The PublishSubject is subsequently removed from the map.

Receiving Actual Cloud Messages

To receive actual cloud messages, the user of the library just declares a BroadcastReceiver in their manifest with the export flag set, the com.google.android.c2dm.permission.SEND permission set, and an intent filter that responds to the com.google.android.c2dm.intent.RECEIVE intent.

Sending

All commands are sent as Intent.

The intent is merged from the key-value Bundle that contains the parameters for the current call, and a set of keys and values that is always the same.

Each intent also gets a unique id set, which can be used to notify a callback left when the original call was made about the response.

That second set is:

Key Value
gsmv The version code of the currently installed Google Play Services version
osv The SDK_INT of the currently installed Android version
app_ver The versionCode of the app making the request (configurable)
app_ver_name The versionName of the app making the request (configurable)
cliv The version of the Cloud Messaging library, currently "fiid-9452000" (configurable)
gmp_app_id The AppId of the Cloud Messaging project (configurable)
appid The fingerprint of the public key of the app (see Keys and Signing section)
pub2 The public key of the app encoded as Base64
sig See Keys and Signing section
kid A field that contains the unique id of the intent, f.e. `
app The callback intent specified below
google.messenger The messenger used to receive messages

All fields, except app and google.messenger, are Strings, those two are Parcelables.

All intents also get the action com.google.android.c2dm.intent.REGISTER and the package of the currently installed Google Play Services version set.

Building the callback intent

The callback intent is always the same, and should be only created once, on demand, and then stored and reused for all future calls.

First, a new intent is created, with only the package defined, which is set to com.google.example.invalidpackage. Then a Pending Broadcast Intent (see PendingIntent#getBroadcast) is created in the current context with the previously created intent, and flags and requestcode both set to zero.

Establishing a connection

To establish a connection, an empty Intent is created, and postprocessed with the function described in "Sending commands".

That intent is then used to start a service from the current context.

Sending Commands

If no connection attempt has been made, the system will try to establish a connection.

If an attempt has been made, but no messenger received yet, commands will be queued.

If a messenger has already been received, a new Message object is obtained, its object set to the Intent that should be sent, and the Messenger’s send method called with it as argument.

Subscribing

To subscribe, an Intent with the following settings is created, postprocessed, and then sent:

Key Value
scope The scope that should be subscribed to (configurable)
sender The senderid of the current project (configurable)
subscription The subscription that this applies to (configurable)
subtype The subtype that should be subscribed to (configurable, but normally identical to the sender)

Unsubscribing

To subscribe, an Intent with the following settings is created, postprocessed, and then sent:

Key Value
scope The scope that should be subscribed to (configurable)
sender The senderid of the current project (configurable)
subscription The subscription that this applies to (configurable)
subtype The subtype that should be subscribed to (configurable, but normally identical to the sender)
delete This field always has the value "1"

Processing received messages

To receive messages at all, a Messenger object is instantiated with a Handler running on the main Looper, with the handleMessage method overriden.

That method receives a Message object, containing only one object that is relevant to us: msg.obj

We will only handle the case where that is an instance of Intent.

The intent has a String extra registration_id, if that is not existing, unsubscribe will be read instead.

The response number is always equivalent to the request number, and can be used to return the response to a callback stored when the request was made.

The processed messages will be passed to the promise that we stored when the call was made, looked up based on the ID number in the returned response.

That property can take these formats:

Set-Messenger

|ID|<number>|:MESSENGER or |ID|<number>|MESSENGER

If that is the case, we extract the google.messenger property from the intent, and replace the messenger we use to send messages back to the GCM process with it.

If this is the first time a messenger is set, we also process all elements from the queue.

Register

|ID|<number>|<token>

If this is the case, have gotten a token which can be used to send messages to the device.

Unregister

|ID|<number>|<application_id>

If this is the case, we successfully unregistered a token from the service.

Ignored

There are also the following possible formats: |ID|<number>|SYNC|<ignored> or |ID|<number>|RST|<ignored>, but they are ignored for now.

Keys and Signing

The library has a keypair which it stores in SharedPreferences.

If none is available, one is created with RSA with 2048 bit length. Otherwise it is read from the preferences.

In the preferences, keys are written Base64 encoded, with the NO_WRAP, NO_PADDING and the URL_SAFE flag set, while for reading only URL_SAFE is set.

Signing

The signature function takes an arbitrary amount of Strings, joins them with a linefeed inbetween, and encodes it in UTF-8.

Afterwards, that set of bytes is then passed to a signature function, SHA256withECDSA or SHA256withRSA depending on which type of key was used, signed, and encoded with Base64 in the same way the keys were stored.

Fingerprint

The fingerprint is computed by taking the SHA1 digest of the public key, and replacing its zeroth byte with the below mentioned function applied to it.

Afterwards, the first 8 bytes are encoded to Base64 with the same flags as the keys were processed with, and returned.

###Byte function

This function processes a byte by first binarily ANDing it with 0xf, adding 0x70 and ANDing it again with 0xff. If the result is not a byte, it is casted to a byte upon that point.

Finding the Google Play Services Package Name and Version

Finding the Version

Android has a utility for this, which is used here:

The package manager is queried for the packageinfo of the package with the Google Play Services package name, and if a result is returned, the version code of that is taken.

If the result is null, or a NameNotFoundException is thrown, -1 is returned instead.

Finding the Package Name

Android provides a useful utility here, too:

The package manager is used again, this time to query for intent services that can handle an Intent with the action set to com.google.android.c2dm.intent.REGISTER.

That returns a list of package names, which is iterated through.

Each is checked for validity, by checking with the packagemanager if that packageName has the com.google.android.c2dm.permission.RECEIVE permission, and, if yes, that package name is returned.

If no such package name is found, we fall back to trial and error:

First we try to get the application info for com.google.android.gms, if existing, we return the package name of that.

If the result is null, or a NameNotFoundException is thrown, we repeat the process for com.google.android.gsf.

If that also does not yield a result, an Exception is thrown instead.

Once a lookup has returned a result, that result is cached and used for all future calls searching for that package name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment