diff --git a/.travis.yml b/.travis.yml index ab31f2699..090ea2249 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,16 @@ sudo: false android: components: - - build-tools-23.0.1 + - platform-tools + - tools + - build-tools-23.0.3 - android-22 - doc-23 - extra-android-support - extra-android-m2repository + - extra-google-m2repository + - extra-google-google_play_services + - addon-google_apis-google-28 before_install: - pip install --user codecov diff --git a/Parse/build.gradle b/Parse/build.gradle index 7a3353b37..3909c007e 100644 --- a/Parse/build.gradle +++ b/Parse/build.gradle @@ -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' diff --git a/Parse/src/main/java/com/parse/GCMService.java b/Parse/src/main/java/com/parse/GCMService.java index 6ca46a3a8..2581c7f7f 100644 --- a/Parse/src/main/java/com/parse/GCMService.java +++ b/Parse/src/main/java/com/parse/GCMService.java @@ -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; @@ -33,6 +36,15 @@ "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 static final String GCM_BODY_EXTRA = "gcm.notification.body"; + private static final String GCM_TITLE_EXTRA = "gcm.notification.title"; + private static final String GCM_SOUND_EXTRA = "gcm.notification.sound"; + private static final String GCM_COMPOSE_ID_EXTRA = "google.c.a.c_id"; + private static final String GCM_COMPOSE_TIMESTAMP_EXTRA = "google.c.a.ts"; private final WeakReference parent; private ExecutorService executor; @@ -83,6 +95,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); } @@ -94,7 +108,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 } @@ -116,19 +136,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(GCM_COMPOSE_ID_EXTRA); + String millisString = intent.getStringExtra(GCM_COMPOSE_TIMESTAMP_EXTRA); + 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_BODY_EXTRA)) { + data.put("alert", intent.getStringExtra(GCM_BODY_EXTRA)); + } + if (intent.hasExtra(GCM_TITLE_EXTRA)) { + data.put("title", intent.getStringExtra(GCM_TITLE_EXTRA)); + } + if (intent.hasExtra(GCM_SOUND_EXTRA)) { + data.put("sound", intent.getStringExtra(GCM_SOUND_EXTRA)); + } + + 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. */ diff --git a/Parse/src/main/java/com/parse/GcmRegistrar.java b/Parse/src/main/java/com/parse/GcmRegistrar.java index d4234a0b1..2bfd5ca70 100644 --- a/Parse/src/main/java/com/parse/GcmRegistrar.java +++ b/Parse/src/main/java/com/parse/GcmRegistrar.java @@ -8,42 +8,32 @@ */ package com.parse; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.os.Bundle; -import android.os.SystemClock; + +import com.google.android.gms.gcm.GcmPubSub; +import com.google.android.gms.iid.InstanceID; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Random; import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicInteger; import bolts.Continuation; import bolts.Task; -import bolts.TaskCompletionSource; /** * A class that manages registering for GCM and updating the registration if it is out of date. */ /** package */ class GcmRegistrar { private static final String TAG = "com.parse.GcmRegistrar"; - private static final String REGISTRATION_ID_EXTRA = "registration_id"; private static final String ERROR_EXTRA = "error"; // Client-side key for parseplatform@gmail.com. See parse/config/gcm.yml for server-side key. private static final String PARSE_SENDER_ID = "1076345567071"; private static final String SENDER_ID_EXTRA = "com.parse.push.gcm_sender_id"; - public static final String REGISTER_ACTION = "com.google.android.c2dm.intent.REGISTER"; - private static final String FILENAME_DEVICE_TOKEN_LAST_MODIFIED = "deviceTokenLastModified"; private long localDeviceTokenLastModified; private final Object localDeviceTokenLastModifiedMutex = new Object(); @@ -66,11 +56,17 @@ private static String actualSenderIDFromExtra(Object senderIDExtra) { return null; } - return senderID.substring(3); + String[] splitIds = senderID.split(","); + splitIds[0] = splitIds[0].substring(3); + if (splitIds.length > 1) { + PLog.w(TAG, "Received registration for multiple sender IDs, which is no longer supported. " + + "Will only use the first sender ID (" + splitIds[0] + ")"); + } + + return splitIds[0]; } private final Object lock = new Object(); - private Request request = null; private Context context = null; // This a package-level constructor for unit testing only. Otherwise, use getInstance(). @@ -118,50 +114,77 @@ public Task then(Task task) throws Exception { } } - private Task sendRegistrationRequestAsync() { + private String getSenderID() { + // Look for an element like this as a child of the element: + // + // + // + // The reason why the "id:" prefix is necessary is because Android treats any metadata value + // that is a string of digits as an integer. So the call to Bundle.getString() will actually + // return null for `android:value="567327206255"`. Additionally, Bundle.getInteger() returns + // a 32-bit integer. For `android:value="567327206255"`, this returns a truncated integer + // because 567327206255 is larger than the largest 32-bit integer. + Bundle metaData = ManifestInfo.getApplicationMetadata(context); + if (metaData != null) { + Object senderIDExtra = metaData.get(SENDER_ID_EXTRA); + + if (senderIDExtra != null) { + String senderID = actualSenderIDFromExtra(senderIDExtra); + + if (senderID != null) { + return senderID; + } else { + PLog.e(TAG, "Found " + SENDER_ID_EXTRA + " element with value \"" + + senderIDExtra.toString() + "\", but the value is missing the expected \"id:\" " + + "prefix."); + } + } + } + + return PARSE_SENDER_ID; + } + + boolean requesting = false; + /** package protected so the GCMService can force a refresh when it gets a notice that the + * InstanceID was invalidated + */ + Task sendRegistrationRequestAsync() { synchronized (lock) { - if (request != null) { + if (requesting) { return Task.forResult(null); } - // Look for an element like this as a child of the element: - // - // - // - // The reason why the "id:" prefix is necessary is because Android treats any metadata value - // that is a string of digits as an integer. So the call to Bundle.getString() will actually - // return null for `android:value="567327206255"`. Additionally, Bundle.getInteger() returns - // a 32-bit integer. For `android:value="567327206255"`, this returns a truncated integer - // because 567327206255 is larger than the largest 32-bit integer. - Bundle metaData = ManifestInfo.getApplicationMetadata(context); - String senderIDs = PARSE_SENDER_ID; - if (metaData != null) { - Object senderIDExtra = metaData.get(SENDER_ID_EXTRA); - - if (senderIDExtra != null) { - String senderID = actualSenderIDFromExtra(senderIDExtra); - - if (senderID != null) { - senderIDs += ("," + senderID); - } else { - PLog.e(TAG, "Found " + SENDER_ID_EXTRA + " element with value \"" + - senderIDExtra.toString() + "\", but the value is missing the expected \"id:\" " + - "prefix."); + requesting = true; + + return Task.callInBackground(new Callable() { + @Override + public String call() throws Exception { + // The InstanceID library already handles backoffs and retries, but I've seen the overall + // process throw a timeout exception. This is just a minor defense in depth against + // transient issues. + Exception lastException = null; + String senderID = getSenderID(); + for (int attempt = 0; attempt < 3; attempt++) { + try { + return InstanceID.getInstance(context).getToken(senderID, "GCM"); + } catch (Exception e) { + lastException = e; + } } + throw lastException; } - } - - request = Request.createAndSend(context, senderIDs); - return request.getTask().continueWith(new Continuation() { + }).continueWith(new Continuation() { @Override public Void then(Task task) { Exception e = task.getError(); if (e != null) { PLog.e(TAG, "Got error when trying to register for GCM push", e); + } else { + setGCMRegistrationId(task.getResult()); } synchronized (lock) { - request = null; + requesting = false; } return null; @@ -171,48 +194,61 @@ public Void then(Task task) { } /** - * Should be called by a broadcast receiver or service to handle the GCM registration response - * intent (com.google.android.c2dm.intent.REGISTRATION). + * On older versions of Android that only support GCM v3, the GCM libraries will require the + * com.google.android.c2dm.intent.REGISTRATION to be handled by a service, which gets trampolined + * back here. On newer versions of Android which support GCM v3, the device ID is generated by + * the InstanceID. The InstanceID is easy to fetch and is relatively stable but can be changed. + * Developers are advised to handle the com.google.android.gms.iid.InstanceID intent and re-fetch + * the InstanceID and tokens. */ - public Task handleRegistrationIntentAsync(Intent intent) { + public Task setGCMRegistrationId(final String registrationId) { List> tasks = new ArrayList<>(); - /* - * We have to parse the response here because GCM may send us a new registration_id - * out-of-band without a request in flight. - */ - String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA); if (registrationId != null && registrationId.length() > 0) { PLog.v(TAG, "Received deviceToken <" + registrationId + "> from GCM."); - ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + final ParseInstallation installation = ParseInstallation.getCurrentInstallation(); // Compare the new deviceToken with the old deviceToken, we only update the // deviceToken if the new one is different from the old one. This does not follow google // guide strictly. But we find most of the time if user just update the app, the // registrationId does not change so there is no need to save it again. if (!registrationId.equals(installation.getDeviceToken())) { + boolean wasAlreadyV4 = installation.isUsingGCMv4(); + installation.setPushType(PushType.GCM); installation.setDeviceToken(registrationId); + + // GCM v4 has a built-in pubsub concept that is redundant with push to channels. It's + // cheaper and dramatically faster to use push to a GCM topic than to use the standard + // mongo pipeline, so we backfill current subscriptions. + if (installation.isUsingGCMv4() && !wasAlreadyV4) { + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + tasks.add(subscribeToGCMTopics(channels)); + } + + // Old versions of the SDK would mint tokens that were reachable from the Parse Push servers + // but used the Parse Sender ID. In the transition to GCM v4 we choose to submit a token + // works with the Parse sender ID _or_ the developer sender ID, but not both (because these + // become two tokens in GCM v4 and the server would need breaking changes to handle this). + // To keep Parse.com push working correctly we need to pin this token to the developer's + // sender ID. + String senderID = getSenderID(); + if (senderID != null && senderID != PARSE_SENDER_ID) { + installation.put("GCMSenderId", senderID); + } + + if (installation.getPushType() != PushType.GCM) { + installation.setPushType(PushType.GCM); + } tasks.add(installation.saveInBackground()); } // We need to update the last modified even the deviceToken is the same. Otherwise when the // app is opened again, isDeviceTokenStale() will always return false so we will send // request to GCM every time. tasks.add(updateLocalDeviceTokenLastModifiedAsync()); - } - synchronized (lock) { - if (request != null) { - request.onReceiveResponseIntent(intent); - } - } - return Task.whenAll(tasks); - } - // Only used by tests. - /* package */ int getRequestIdentifier() { - synchronized (lock) { - return request != null ? request.identifier : 0; } + return Task.whenAll(tasks); } /** package for tests */ Task isLocalDeviceTokenStaleAsync() { @@ -225,6 +261,70 @@ public Task then(Task task) throws Exception { }); } + private static final String GCM_TOPIC_PATTERN = "^[0-9a-zA-Z-_.~%]{1,900}$"; + private static final String GCM_TOPIC_PREFIX = "/topics/"; + + /* Used during channel subscription to keep GCM topics in sync. When a device first upgrades to + * GCM v4 it sends all prior channels to backfill GCM. + */ + Task subscribeToGCMTopics(final List channels) { + final ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + if (!installation.isUsingGCMv4()) { + return Task.forResult(null); + } + + return Task.callInBackground(new Callable() { + @Override + public GcmPubSub call() throws Exception { + return GcmPubSub.getInstance(context); + } + }).continueWithTask(new Continuation>() { + public Task then(Task task) { + final GcmPubSub pubSub = task.getResult(); + final String registrationId = installation.getDeviceToken(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + List> registrations = new ArrayList>(); + + for (String channel: channels) { + if (!channel.matches(GCM_TOPIC_PATTERN)) { + PLog.w(TAG, "Cannot subscribe channel " + channel + " as a GCM topic because it is an" + + "invalid GCM topic name"); + continue; + } + + final String topic = GCM_TOPIC_PREFIX + channel; + Task registration = Task.callInBackground(new Callable() { + @Override + public Void call() throws Exception { + pubSub.subscribe(registrationId, topic, null /* extras */); + return null; + } + }); + registrations.add(registration); + } + + return Task.whenAll(registrations); + } + }); + } + + Task unsubscribeFromGCMTopic(final String channel) { + final ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + if (!installation.isUsingGCMv4() || !channel.matches(GCM_TOPIC_PATTERN)) { + return Task.forResult(null); + } + + return Task.callInBackground(new Callable() { + @Override + public Void call() throws Exception { + GcmPubSub pubsub = GcmPubSub.getInstance(context); + String topic = GCM_TOPIC_PREFIX + channel; + pubsub.unsubscribe(installation.getDeviceToken(), topic); + return null; + } + }); + } + /** package for tests */ Task updateLocalDeviceTokenLastModifiedAsync() { return Task.call(new Callable() { @Override @@ -273,125 +373,4 @@ public Long call() throws Exception { /** package for tests */ static void deleteLocalDeviceTokenLastModifiedFile() { ParseFileUtils.deleteQuietly(getLocalDeviceTokenLastModifiedFile()); } - - /** - * Encapsulates the a GCM registration request-response, potentially using AlarmManager to - * schedule retries if the GCM service is not available. - */ - private static class Request { - private static final String RETRY_ACTION = "com.parse.RetryGcmRegistration"; - private static final int MAX_RETRIES = 5; - private static final int BACKOFF_INTERVAL_MS = 3000; - - final private Context context; - final private String senderId; - final private Random random; - final private int identifier; - final private TaskCompletionSource tcs; - final private PendingIntent appIntent; - final private AtomicInteger tries; - final private PendingIntent retryIntent; - final private BroadcastReceiver retryReceiver; - - public static Request createAndSend(Context context, String senderId) { - Request request = new Request(context, senderId); - request.send(); - - return request; - } - - private Request(Context context, String senderId) { - this.context = context; - this.senderId = senderId; - this.random = new Random(); - this.identifier = this.random.nextInt(); - this.tcs = new TaskCompletionSource<>(); - this.appIntent = PendingIntent.getBroadcast(this.context, identifier, new Intent(), 0); - this.tries = new AtomicInteger(0); - - String packageName = this.context.getPackageName(); - Intent intent = new Intent(RETRY_ACTION).setPackage(packageName); - intent.addCategory(packageName); - intent.putExtra("random", identifier); - this.retryIntent = PendingIntent.getBroadcast(this.context, identifier, intent, 0); - - this.retryReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent != null && intent.getIntExtra("random", 0) == identifier) { - send(); - } - } - }; - - IntentFilter filter = new IntentFilter(); - filter.addAction(RETRY_ACTION); - filter.addCategory(packageName); - - context.registerReceiver(this.retryReceiver, filter); - } - - public Task getTask() { - return tcs.getTask(); - } - - private void send() { - Intent intent = new Intent(REGISTER_ACTION); - intent.setPackage("com.google.android.gsf"); - intent.putExtra("sender", senderId); - intent.putExtra("app", appIntent); - - ComponentName name = null; - try { - name = context.startService(intent); - } catch (SecurityException exception) { - // do nothing - } - - if (name == null) { - finish(null, "GSF_PACKAGE_NOT_AVAILABLE"); - } - - tries.incrementAndGet(); - - PLog.v(TAG, "Sending GCM registration intent"); - } - - public void onReceiveResponseIntent(Intent intent) { - String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA); - String error = intent.getStringExtra(ERROR_EXTRA); - - if (registrationId == null && error == null) { - PLog.e(TAG, "Got no registration info in GCM onReceiveResponseIntent"); - return; - } - - // Retry with exponential backoff if GCM isn't available. - if ("SERVICE_NOT_AVAILABLE".equals(error) && tries.get() < MAX_RETRIES) { - AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP; - long delay = (1 << tries.get()) * BACKOFF_INTERVAL_MS + random.nextInt(BACKOFF_INTERVAL_MS); - long start = SystemClock.elapsedRealtime() + delay; - manager.set(alarmType, start, retryIntent); - } else { - finish(registrationId, error); - } - } - - private void finish(String registrationId, String error) { - boolean didSetResult; - - if (registrationId != null) { - didSetResult = tcs.trySetResult(registrationId); - } else { - didSetResult = tcs.trySetError(new Exception("GCM registration error: " + error)); - } - - if (didSetResult) { - appIntent.cancel(); - retryIntent.cancel(); - context.unregisterReceiver(this.retryReceiver); - } - } - } } diff --git a/Parse/src/main/java/com/parse/ManifestInfo.java b/Parse/src/main/java/com/parse/ManifestInfo.java index 96451570e..8dd41a081 100644 --- a/Parse/src/main/java/com/parse/ManifestInfo.java +++ b/Parse/src/main/java/com/parse/ManifestInfo.java @@ -238,7 +238,7 @@ public static PushType getPushType() { ParsePushBroadcastReceiver.ACTION_PUSH_DELETE + ". You can do this by adding " + "these lines to your AndroidManifest.xml:\n\n" + " \n" + + " android:exported=\"false\">\n" + " \n" + " \n" + " \n" + @@ -291,9 +291,14 @@ public static PushType getPushType() { * but push isn't actually enabled because the manifest is misconfigured. */ public static String getNonePushTypeLogMessage() { - return "Push is not configured for this app because the app manifest is missing required " + - "declarations. Please add the following declarations to your app manifest to use GCM for " + - "push: " + getGcmManifestMessage(); + if (isGooglePlayServicesAvailable()) { + return "Push is not configured for this app because the app manifest is missing required " + + "declarations. Please add the following declarations to your app manifest to use GCM for " + + "push: " + getGcmManifestMessage(); + } else { + return "Push is not available on this device because Google Play Services are not available " + + "on this device and PPNS is not enabled in your app manifest."; + } } enum ManifestCheckResult { @@ -500,7 +505,11 @@ private static ManifestCheckResult gcmSupportLevel() { } String[] optionalPermissions = new String[] { - "android.permission.VIBRATE" + "android.permission.VIBRATE", + + // Technically this is optional, but the app will not get updates to expired push tokens until + // it performs another manual check. + "com.google.android.gms.iid.InstanceID" }; if (!hasGrantedPermissions(context, optionalPermissions)) { @@ -570,6 +579,7 @@ private static String getGcmManifestMessage() { " \n" + " \n" + " \n" + + " \n" + " \n" + " \n" + "\n" + diff --git a/Parse/src/main/java/com/parse/ParseInstallation.java b/Parse/src/main/java/com/parse/ParseInstallation.java index 70019e314..91d57a425 100644 --- a/Parse/src/main/java/com/parse/ParseInstallation.java +++ b/Parse/src/main/java/com/parse/ParseInstallation.java @@ -243,6 +243,11 @@ private void updateLocaleIdentifier() { } } + /* package */ boolean isUsingGCMv4() { + String token = getDeviceToken(); + return token != null && token.indexOf(':') != -1; + } + // TODO(mengyan): Move to ParseInstallationInstanceController /* package */ void updateDeviceInfo() { updateDeviceInfo(ParsePlugins.get().installationId()); diff --git a/Parse/src/main/java/com/parse/ParseNotificationManager.java b/Parse/src/main/java/com/parse/ParseNotificationManager.java index f206dadc9..b80164894 100644 --- a/Parse/src/main/java/com/parse/ParseNotificationManager.java +++ b/Parse/src/main/java/com/parse/ParseNotificationManager.java @@ -58,14 +58,14 @@ public void showNotification(Context context, Notification notification) { (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // Pick an id that probably won't overlap anything - int notificationId = (int)System.currentTimeMillis(); + int id = (int) System.currentTimeMillis(); try { - nm.notify(notificationId, notification); + nm.notify(id, notification); } catch (SecurityException e) { // Some phones throw an exception for unapproved vibration notification.defaults = Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND; - nm.notify(notificationId, notification); + nm.notify(id, notification); } } } diff --git a/Parse/src/main/java/com/parse/ParsePushChannelsController.java b/Parse/src/main/java/com/parse/ParsePushChannelsController.java index 02cd77b10..d6934c15b 100644 --- a/Parse/src/main/java/com/parse/ParsePushChannelsController.java +++ b/Parse/src/main/java/com/parse/ParsePushChannelsController.java @@ -8,6 +8,8 @@ */ package com.parse; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,16 +31,19 @@ public Task subscribeInBackground(final String channel) { return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { - ParseInstallation installation = task.getResult(); + final ParseInstallation installation = task.getResult(); + List> tasks = new ArrayList>(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); if (channels == null || installation.isDirty(ParseInstallation.KEY_CHANNELS) || !channels.contains(channel)) { installation.addUnique(ParseInstallation.KEY_CHANNELS, channel); - return installation.saveInBackground(); - } else { - return Task.forResult(null); + tasks.add(installation.saveInBackground()); + tasks.add(GcmRegistrar.getInstance().subscribeToGCMTopics(Arrays.asList(channel))); } + + return Task.whenAll(tasks); } }); } @@ -51,15 +56,18 @@ public Task unsubscribeInBackground(final String channel) { return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { - ParseInstallation installation = task.getResult(); + final ParseInstallation installation = task.getResult(); + List> tasks = new ArrayList>(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); if (channels != null && channels.contains(channel)) { installation.removeAll( ParseInstallation.KEY_CHANNELS, Collections.singletonList(channel)); - return installation.saveInBackground(); - } else { - return Task.forResult(null); + tasks.add(installation.saveInBackground()); + tasks.add(GcmRegistrar.getInstance().unsubscribeFromGCMTopic(channel)); } + + return Task.whenAll(tasks); } }); } diff --git a/build.gradle b/build.gradle index e1feb49c5..42b8b35db 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:2.1.0' } } @@ -16,7 +16,7 @@ allprojects { ext { compileSdkVersion = 22 - buildToolsVersion = "23.0.1" + buildToolsVersion = "23.0.3" minSdkVersion = 9 targetSdkVersion = 23 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cd481edfb..ae5079024 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Nov 06 11:00:14 PST 2014 +#Mon May 09 18:14:42 PDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip