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

Commit 6fe852b

Browse files
committed
Upgrade Parse Push to GCM v4
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?
1 parent 95b6b17 commit 6fe852b

7 files changed

+270
-212
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

+60-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,15 @@
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";
43+
private static final String GCM_BODY_EXTRA = "gcm.notification.body";
44+
private static final String GCM_TITLE_EXTRA = "gcm.notification.title";
45+
private static final String GCM_SOUND_EXTRA = "gcm.notification.sound";
46+
private static final String GCM_COMPOSE_ID_EXTRA = "google.c.a.c_id";
47+
private static final String GCM_COMPOSE_TIMESTAMP_EXTRA = "google.c.a.ts";
3648

3749
private final WeakReference<Service> parent;
3850
private ExecutorService executor;
@@ -83,6 +95,8 @@ private void onHandleIntent(Intent intent) {
8395
handleGcmRegistrationIntent(intent);
8496
} else if (RECEIVE_PUSH_ACTION.equals(action)) {
8597
handleGcmPushIntent(intent);
98+
} else if (INSTANCE_ID_ACTION.equals(action)) {
99+
handleInvalidatedInstanceId(intent);
86100
} else {
87101
PLog.e(TAG, "PushService got unknown intent in GCM mode: " + intent);
88102
}
@@ -94,7 +108,13 @@ private void handleGcmRegistrationIntent(Intent intent) {
94108
// Have to block here since GCMService is basically an IntentService, and the service is
95109
// may exit before async handling of the registration is complete if we don't wait for it to
96110
// complete.
97-
GcmRegistrar.getInstance().handleRegistrationIntentAsync(intent).waitForCompletion();
111+
PLog.d(TAG, "Got registration intent in service");
112+
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
113+
// Multiple IDs come back to the legacy intnet-based API with an |ID|num: prefix; cut it off.
114+
if (registrationId.startsWith("|ID|")) {
115+
registrationId = registrationId.substring(registrationId.indexOf(':') + 1);
116+
}
117+
GcmRegistrar.getInstance().setGCMRegistrationId(registrationId).waitForCompletion();
98118
} catch (InterruptedException e) {
99119
// do nothing
100120
}
@@ -116,19 +136,52 @@ private void handleGcmPushIntent(Intent intent) {
116136
String channel = intent.getStringExtra("channel");
117137

118138
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;
139+
try {
140+
if (dataString != null) {
141+
data = new JSONObject(dataString);
142+
} else if (pushId == null && timestamp == null && dataString == null && channel == null) {
143+
// The Parse SDK is older than GCM, so it has some non-standard payload fields.
144+
// This allows the Parse SDK to handle push providers using the now-standard GCM payloads.
145+
pushId = intent.getStringExtra(GCM_COMPOSE_ID_EXTRA);
146+
String millisString = intent.getStringExtra(GCM_COMPOSE_TIMESTAMP_EXTRA);
147+
if (millisString != null) {
148+
Long millis = Long.valueOf(millisString);
149+
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
150+
timestamp = df.format(new Date(millis));
151+
}
152+
data = new JSONObject();
153+
if (intent.hasExtra(GCM_BODY_EXTRA)) {
154+
data.put("alert", intent.getStringExtra(GCM_BODY_EXTRA));
155+
}
156+
if (intent.hasExtra(GCM_TITLE_EXTRA)) {
157+
data.put("title", intent.getStringExtra(GCM_TITLE_EXTRA));
158+
}
159+
if (intent.hasExtra(GCM_SOUND_EXTRA)) {
160+
data.put("sound", intent.getStringExtra(GCM_SOUND_EXTRA));
161+
}
162+
163+
String from = intent.getStringExtra("from");
164+
if (from != null && from.startsWith("/topics/")) {
165+
channel = from.substring("/topics/".length());
166+
}
125167
}
168+
} catch (JSONException e) {
169+
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
170+
return;
126171
}
127172

128173
PushRouter.getInstance().handlePush(pushId, timestamp, channel, data);
129174
}
130175
}
131176

177+
private void handleInvalidatedInstanceId(Intent intent) {
178+
try {
179+
GcmRegistrar.getInstance().sendRegistrationRequestAsync().waitForCompletion();
180+
} catch (InterruptedException e) {
181+
// do nothing
182+
}
183+
}
184+
132185
/**
133186
* Stop the parent Service, if we're still running.
134187
*/

0 commit comments

Comments
 (0)