Skip to content
This repository has been archived by the owner on Sep 7, 2022. It is now read-only.

Commit

Permalink
Upgrade Parse Push to GCM v4
Browse files Browse the repository at this point in the history
This change:
----------

* Moves Parse back to using public APIs (open [GitHub
* discussion](ParsePlatform#445))
* Cleans up a lot of code in `GcmRegistrar` that is redundant with GCM
* APIs or written before Bolts
* Fixes a typo in manifest instructions that used a literal `bool`
* instead of `"bool"`
* Fixes a bug where ParseInstallation did not save the GcmSenderId,
* causing Parse to not use the developer's secrets.
* Fixes a bug where Parse incorrectly blames a manifest error when GCM
* is unavailable because the device doesn't have Play Services.
* Add a compatibility shim that lets `ParsePushBroadcastReceiver`
* correctly handle the standard payloads expected by
* [com.android.gms.gcm.GcmReceiver](https://developers.google.com/android/reference/com/google/android/gms/gcm/GcmReceiver).
* This lets customers who previously used another push provider use the
* `ParsePushBroadcastReceiver` instead.
* Add support for GCMv4, including a new optional intent to notify the
* app when the InstanceID is invalidated.

GCM v4 has a number of benefits:
---------------

* GCM v4 is based on a device-owned InstanceID. Push tokens are oauth
* tokens signed by the device, so this fixes double-send bugs that Parse
* Push has never been able to fix.
* If we used the InstanceID as the ParseInstallation.InstallationId, we
* would also increase stability of the Installation record, which fixes
* some cases where Installations are wiped & replaced (related to the
* above bug for senderId stability).
* This API has a callback in case the InstanceID is invalidated, which
* should reduce client/server inconsistencies.
* These tokens support new server-side APIs like push-to-topic, which
* are _dramatically_ faster than the normal ParsePush path.
* When a device upgrades to GCMv4, the device keeps GCM topics in sync
* with channels. This paves the way to implement push-to-channels on top
* of topics. It also allows the customer to keep some of their targeting
* info regardless of which push provider they choose to use.

This has two possibly controversial requirements:
----------------

* The new API issues one token per sender ID rather than one token that
* works with all sender IDs. To avoid an invasive/breaking server-side
* change, we are _no longer requesting tokens for the Parse sender ID_
* if the developer provided their own. We will also only support at most
* one custom sender ID. I've had a number of conversations about this
* and nobody seems concerned.
* This change introduces a dependency on the Google Mobile Services SDK.
* The dependency is just the GCM .jar and does _not_ limit the Parse SDK
* to devices with Play Services (tested on an ICS emulator w/o Google
* APIs). I originally tried doing this without the dependency, but the
* new API has a large amount of crypto and incredible care for compat
* shims on older API levels. I assume my hand-crafted copy would be
* worse quality.

Open questions
-----------------
* Should Parse use the GMS InstanceID over the InstallationId when
* available? This makes the server-side Installation deduplication code
* work better, but could break systems that assume InstallationId is a
* UUID.
* Google workflows provide a `google-services.json` file that GMS uses
* to auto-initialize various Google products (including GCM). Should we
* allow the Parse SDK to initialize the developer's sender ID with this
* file in addition to the Parse-specific way?
  • Loading branch information
inlined committed May 4, 2016
1 parent 95b6b17 commit 6326327
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 212 deletions.
1 change: 1 addition & 0 deletions Parse/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ android {

dependencies {
compile 'com.parse.bolts:bolts-tasks:1.4.0'
compile 'com.google.android.gms:play-services-gcm:8.4.0'

provided 'com.squareup.okhttp:okhttp:2.4.0'
provided 'com.facebook.stetho:stetho:1.1.1'
Expand Down
62 changes: 55 additions & 7 deletions Parse/src/main/java/com/parse/GCMService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Expand All @@ -33,6 +36,10 @@
"com.google.android.c2dm.intent.REGISTRATION";
public static final String RECEIVE_PUSH_ACTION =
"com.google.android.c2dm.intent.RECEIVE";
public static final String INSTANCE_ID_ACTION =
"com.google.android.gms.iid.InstanceID";

private static final String REGISTRATION_ID_EXTRA = "registration_id";

private final WeakReference<Service> parent;
private ExecutorService executor;
Expand Down Expand Up @@ -83,6 +90,8 @@ private void onHandleIntent(Intent intent) {
handleGcmRegistrationIntent(intent);
} else if (RECEIVE_PUSH_ACTION.equals(action)) {
handleGcmPushIntent(intent);
} else if (INSTANCE_ID_ACTION.equals(action)) {
handleInvalidatedInstanceId(intent);
} else {
PLog.e(TAG, "PushService got unknown intent in GCM mode: " + intent);
}
Expand All @@ -94,7 +103,13 @@ private void handleGcmRegistrationIntent(Intent intent) {
// Have to block here since GCMService is basically an IntentService, and the service is
// may exit before async handling of the registration is complete if we don't wait for it to
// complete.
GcmRegistrar.getInstance().handleRegistrationIntentAsync(intent).waitForCompletion();
PLog.d(TAG, "Got registration intent in service");
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
// Multiple IDs come back to the legacy intnet-based API with an |ID|num: prefix; cut it off.
if (registrationId.startsWith("|ID|")) {
registrationId = registrationId.substring(registrationId.indexOf(':') + 1);
}
GcmRegistrar.getInstance().setGCMRegistrationId(registrationId).waitForCompletion();
} catch (InterruptedException e) {
// do nothing
}
Expand All @@ -116,19 +131,52 @@ private void handleGcmPushIntent(Intent intent) {
String channel = intent.getStringExtra("channel");

JSONObject data = null;
if (dataString != null) {
try {
data = new JSONObject(dataString);
} catch (JSONException e) {
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
return;
try {
if (dataString != null) {
data = new JSONObject(dataString);
} else if (pushId == null && timestamp == null && dataString == null && channel == null) {
// The Parse SDK is older than GCM, so it has some non-standard payload fields.
// This allows the Parse SDK to handle push providers using the now-standard GCM payloads.
pushId = intent.getStringExtra("google.c.a.c_id");
String millisString = intent.getStringExtra("google.c.a.ts");
if (millisString != null) {
Long millis = Long.valueOf(millisString);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
timestamp = df.format(new Date(millis));
}
data = new JSONObject();
if (intent.hasExtra("gcm.notification.body")) {
data.put("alert", intent.getStringExtra("gcm.notification.body"));
}
if (intent.hasExtra("gcm.notification.title")) {
data.put("title", intent.getStringExtra("gcm.notification.title"));
}
if (intent.hasExtra("gcm.notification.sound")) {
data.put("sound", intent.getStringExtra("gcm.notification.sound"));
}

String from = intent.getStringExtra("from");
if (from != null && from.startsWith("/topics/")) {
channel = from.substring("/topics/".length());
}
}
} catch (JSONException e) {
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
return;
}

PushRouter.getInstance().handlePush(pushId, timestamp, channel, data);
}
}

private void handleInvalidatedInstanceId(Intent intent) {
try {
GcmRegistrar.getInstance().sendRegistrationRequestAsync().waitForCompletion();
} catch (InterruptedException e) {
// do nothing
}
}

/**
* Stop the parent Service, if we're still running.
*/
Expand Down

0 comments on commit 6326327

Please sign in to comment.