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.
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.
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);
}
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.
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.
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.
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.
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.
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.
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.
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) |
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" |
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:
|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.
|ID|<number>|<token>
If this is the case, have gotten a token which can be used to send messages to the device.
|ID|<number>|<application_id>
If this is the case, we successfully unregistered a token from the service.
There are also the following possible formats: |ID|<number>|SYNC|<ignored>
or
|ID|<number>|RST|<ignored>
, but they are ignored for now.
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.
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.
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.
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.
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.