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

Commit 0b50778

Browse files
committed
Upgrad ParsePush to use GCMv4 on GMS devices.
This change: * Moves Parse back to using public APIs [GitHub discussion](parse-community#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 and Parse didn't use the developer's secrets. * Fixes a bug where Parse incorrectly blames an improperly configured manifest when GCM isn't available because Play Services aren't available. * Add a compatibility shim when reading payloads so customers whose push provider uses the (now) standard intent structure expected by [GcmReceiver](https://developers.google.com/android/reference/com/google/android/gms/gcm/GcmReceiver) can use the ParsePushBroadcastReceiver instead. * Add support for GCMv4, including a new optional intent to be notified InstanceIDs are invalidated client-side. The new protocol has a number of benefits: * The new GCM v4 model is more stable; it is based on a device-owned InstanceID. Push tokens are OAuth tokens to that device. * As a result of the above, this should fix a Parse Push bug where the first GCM push after an app upgrade might be doubled. * This API has a callback in case the InstnaceID 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 (or a customer to try another push provider by next Jan and retain some targeting info). 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 registering 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.
1 parent 95b6b17 commit 0b50778

8 files changed

+312
-226
lines changed

Parse/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ android {
4141

4242
dependencies {
4343
compile 'com.parse.bolts:bolts-tasks:1.4.0'
44+
compile 'com.google.android.gms:play-services-gcm:8.4.0'
4445

4546
provided 'com.squareup.okhttp:okhttp:2.4.0'
4647
provided 'com.facebook.stetho:stetho:1.1.1'

Parse/src/main/java/com/parse/GCMService.java

+55-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import org.json.JSONObject;
1717

1818
import java.lang.ref.WeakReference;
19+
import java.text.DateFormat;
20+
import java.text.SimpleDateFormat;
21+
import java.util.Date;
1922
import java.util.concurrent.ExecutorService;
2023
import java.util.concurrent.Executors;
2124

@@ -33,6 +36,10 @@
3336
"com.google.android.c2dm.intent.REGISTRATION";
3437
public static final String RECEIVE_PUSH_ACTION =
3538
"com.google.android.c2dm.intent.RECEIVE";
39+
public static final String INSTANCE_ID_ACTION =
40+
"com.google.android.gms.iid.InstanceID";
41+
42+
private static final String REGISTRATION_ID_EXTRA = "registration_id";
3643

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

118133
JSONObject data = null;
119-
if (dataString != null) {
120-
try {
121-
data = new JSONObject(dataString);
122-
} catch (JSONException e) {
123-
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
124-
return;
134+
try {
135+
if (dataString != null) {
136+
data = new JSONObject(dataString);
137+
} else if (pushId == null && timestamp == null && dataString == null && channel == null) {
138+
// The Parse SDK is older than GCM, so it has some non-standard payload fields.
139+
// This allows the Parse SDK to handle push providers using the now-standard GCM payloads.
140+
pushId = intent.getStringExtra("google.c.a.c_id");
141+
String millisString = intent.getStringExtra("google.c.a.ts");
142+
if (millisString != null) {
143+
Long millis = Long.valueOf(millisString);
144+
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
145+
timestamp = df.format(new Date(millis));
146+
}
147+
data = new JSONObject();
148+
if (intent.hasExtra("gcm.notification.body")) {
149+
data.put("alert", intent.getStringExtra("gcm.notification.body"));
150+
}
151+
if (intent.hasExtra("gcm.notification.title")) {
152+
data.put("title", intent.getStringExtra("gcm.notification.title"));
153+
}
154+
if (intent.hasExtra("gcm.notification.sound")) {
155+
data.put("sound", intent.getStringExtra("gcm.notification.sound"));
156+
}
157+
158+
String from = intent.getStringExtra("from");
159+
if (from != null && from.startsWith("/topics/")) {
160+
channel = from.substring("/topics/".length());
161+
}
125162
}
163+
} catch (JSONException e) {
164+
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
165+
return;
126166
}
127167

128168
PushRouter.getInstance().handlePush(pushId, timestamp, channel, data);
129169
}
130170
}
131171

172+
private void handleInvalidatedInstanceId(Intent intent) {
173+
try {
174+
GcmRegistrar.getInstance().sendRegistrationRequestAsync().waitForCompletion();
175+
} catch (InterruptedException e) {
176+
// do nothing
177+
}
178+
}
179+
132180
/**
133181
* Stop the parent Service, if we're still running.
134182
*/

0 commit comments

Comments
 (0)