author | Nick Alexander <nalexander@mozilla.com> |
Wed, 02 Mar 2016 16:04:02 -0800 | |
changeset 324857 | 15826b103fd0ac87294bc8c0e913f9916aa0283c |
parent 324856 | f869f0c13f9062fe26a9bafd63392205581673c4 |
child 324858 | 0d112a6d618c9730bddd665f65d75eb60873e9b6 |
push id | 1128 |
push user | jlund@mozilla.com |
push date | Wed, 01 Jun 2016 01:31:59 +0000 |
treeherder | mozilla-release@fe0d30de989d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rnewman |
bugs | 1207714 |
milestone | 47.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
new file mode 100644 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java @@ -0,0 +1,109 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push; + +import android.support.annotation.NonNull; + +import org.mozilla.gecko.push.RegisterUserAgentResponse; +import org.mozilla.gecko.push.SubscribeChannelResponse; +import org.mozilla.gecko.push.autopush.AutopushClient; +import org.mozilla.gecko.push.autopush.AutopushClientException; +import org.mozilla.gecko.sync.Utils; + +import java.util.concurrent.Executor; + +/** + * This class bridges the autopush client, which is written in callback style, with the Fennec + * push implementation, which is written in a linear style. It handles returning results and + * re-throwing exceptions passed as messages. + * <p/> + * TODO: fold this into the autopush client directly. + */ +public class PushClient { + public static class LocalException extends Exception { + private static final long serialVersionUID = 2387554736L; + + public LocalException(Throwable throwable) { + super(throwable); + } + } + + private final AutopushClient autopushClient; + + public PushClient(String serverURI) { + this.autopushClient = new AutopushClient(serverURI, Utils.newSynchronousExecutor()); + } + + /** + * Each instance is <b>single-use</b>! Exactly one delegate method should be invoked once, + * but we take care to handle multiple invocations (favoring the earliest), just to be safe. + */ + protected static class Delegate<T> implements AutopushClient.RequestDelegate<T> { + Object result; // Oh, for an algebraic data type when you need one! + + @SuppressWarnings("unchecked") + public T responseOrThrow() throws LocalException, AutopushClientException { + if (result instanceof LocalException) { + throw (LocalException) result; + } + if (result instanceof AutopushClientException) { + throw (AutopushClientException) result; + } + return (T) result; + } + + @Override + public void handleError(Exception e) { + if (result == null) { + result = new LocalException(e); + } + } + + @Override + public void handleFailure(AutopushClientException e) { + if (result == null) { + result = e; + } + } + + @Override + public void handleSuccess(T response) { + if (result == null) { + result = response; + } + } + } + + public RegisterUserAgentResponse registerUserAgent(@NonNull String token) throws LocalException, AutopushClientException { + final Delegate<RegisterUserAgentResponse> delegate = new Delegate<>(); + autopushClient.registerUserAgent(token, delegate); + return delegate.responseOrThrow(); + } + + public void reregisterUserAgent(@NonNull String uaid, @NonNull String secret, @NonNull String token) throws LocalException, AutopushClientException { + final Delegate<Void> delegate = new Delegate<>(); + autopushClient.reregisterUserAgent(uaid, secret, token, delegate); + delegate.responseOrThrow(); // For side-effects only. + } + + public void unregisterUserAgent(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException { + final Delegate<Void> delegate = new Delegate<>(); + autopushClient.unregisterUserAgent(uaid, secret, delegate); + delegate.responseOrThrow(); // For side-effects only. + } + + public SubscribeChannelResponse subscribeChannel(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException { + final Delegate<SubscribeChannelResponse> delegate = new Delegate<>(); + autopushClient.subscribeChannel(uaid, secret, delegate); + return delegate.responseOrThrow(); + } + + public void unsubscribeChannel(@NonNull String uaid, @NonNull String secret, @NonNull String chid) throws LocalException, AutopushClientException { + final Delegate<Void> delegate = new Delegate<>(); + autopushClient.unsubscribeChannel(uaid, secret, chid, delegate); + delegate.responseOrThrow(); // For side-effects only. + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java @@ -0,0 +1,353 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push; + +import android.support.annotation.NonNull; +import android.util.Log; + +import org.json.JSONObject; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.gcm.GcmTokenClient; +import org.mozilla.gecko.push.autopush.AutopushClientException; +import org.mozilla.gecko.util.ThreadUtils; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * The push manager advances push registrations, ensuring that the upstream autopush endpoint has + * a fresh GCM token. It brokers channel subscription requests to the upstream and maintains + * local state. + * <p/> + * This class is not thread safe. An individual instance should be accessed on a single + * (background) thread. + */ +public class PushManager { + public static final long TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L; // One week. + + public static class ProfileNeedsConfigurationException extends Exception { + private static final long serialVersionUID = 3326738888L; + + public ProfileNeedsConfigurationException() { + super(); + } + } + + private static final String LOG_TAG = "GeckoPushManager"; + + protected final @NonNull PushState state; + protected final @NonNull GcmTokenClient gcmClient; + protected final @NonNull PushClientFactory pushClientFactory; + + // For testing only. + public interface PushClientFactory { + PushClient getPushClient(String autopushEndpoint, boolean debug); + } + + public PushManager(@NonNull PushState state, @NonNull GcmTokenClient gcmClient, @NonNull PushClientFactory pushClientFactory) { + this.state = state; + this.gcmClient = gcmClient; + this.pushClientFactory = pushClientFactory; + } + + public PushRegistration registrationForSubscription(String chid) { + // chids are globally unique, so we're not concerned about finding a chid associated to + // any particular profile. + for (Map.Entry<String, PushRegistration> entry : state.getRegistrations().entrySet()) { + final PushSubscription subscription = entry.getValue().getSubscription(chid); + if (subscription != null) { + return entry.getValue(); + } + } + return null; + } + + public Map<String, PushSubscription> allSubscriptionsForProfile(String profileName) { + final PushRegistration registration = state.getRegistration(profileName); + if (registration == null) { + return Collections.EMPTY_MAP; + } + return Collections.unmodifiableMap(registration.subscriptions); + } + + public PushRegistration registerUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { + Log.i(LOG_TAG, "Registering user agent for profile named: " + profileName); + return advanceRegistration(profileName, now); + } + + public PushRegistration unregisterUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException { + Log.i(LOG_TAG, "Unregistering user agent for profile named: " + profileName); + + final PushRegistration registration = state.getRegistration(profileName); + if (registration == null) { + Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote uaid for profileName: " + profileName); + return null; + } + + final String uaid = registration.uaid.value; + final String secret = registration.secret; + if (uaid == null || secret == null) { + Log.e(LOG_TAG, "Cannot unregisterUserAgent with null registration uaid or secret!"); + return null; + } + + unregisterUserAgentOnBackgroundThread(registration); + return registration; + } + + public PushSubscription subscribeChannel(final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { + Log.i(LOG_TAG, "Subscribing to channel for service: " + service + "; for profile named: " + profileName); + final PushRegistration registration = advanceRegistration(profileName, now); + final PushSubscription subscription = subscribeChannel(registration, profileName, service, serviceData, System.currentTimeMillis()); + return subscription; + } + + protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, final long now) throws AutopushClientException, PushClient.LocalException { + final String uaid = registration.uaid.value; + final String secret = registration.secret; + if (uaid == null || secret == null) { + throw new IllegalStateException("Cannot subscribeChannel with null uaid or secret!"); + } + + // Verify endpoint is not null? + final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); + + final SubscribeChannelResponse result = pushClient.subscribeChannel(uaid, secret); + if (registration.debug) { + Log.i(LOG_TAG, "Got chid: " + result.channelID + " and endpoint: " + result.endpoint); + } else { + Log.i(LOG_TAG, "Got chid and endpoint."); + } + + final PushSubscription subscription = new PushSubscription(result.channelID, profileName, result.endpoint, service, serviceData); + registration.putSubscription(result.channelID, subscription); + state.checkpoint(); + + return subscription; + } + + public PushSubscription unsubscribeChannel(final @NonNull String chid) { + Log.i(LOG_TAG, "Unsubscribing from channel with chid: " + chid); + + final PushRegistration registration = registrationForSubscription(chid); + if (registration == null) { + Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote subscription: " + chid); + return null; + } + + // We remove the local subscription before the remote subscription: without the local + // subscription we'll ignoring incoming messages, and after some amount of time the + // server will expire the channel due to non-activity. This is also Desktop's approach. + final PushSubscription subscription = registration.removeSubscription(chid); + state.checkpoint(); + + if (subscription == null) { + // This should never happen. + Log.e(LOG_TAG, "Subscription did not exist: " + chid); + return null; + } + + final String uaid = registration.uaid.value; + final String secret = registration.secret; + if (uaid == null || secret == null) { + Log.e(LOG_TAG, "Cannot unsubscribeChannel with null registration uaid or secret!"); + return null; + } + + final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); + // Fire and forget. + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + try { + pushClient.unsubscribeChannel(registration.uaid.value, registration.secret, chid); + Log.i(LOG_TAG, "Unsubscribed from channel with chid: " + chid); + } catch (PushClient.LocalException | AutopushClientException e) { + Log.w(LOG_TAG, "Failed to unsubscribe from channel with chid; ignoring: " + chid, e); + } + } + }); + + return subscription; + } + + public PushRegistration configure(final @NonNull String profileName, final @NonNull String endpoint, final boolean debug, final long now) { + Log.i(LOG_TAG, "Updating configuration."); + final PushRegistration registration = state.getRegistration(profileName); + final PushRegistration newRegistration; + if (registration != null) { + if (!endpoint.equals(registration.autopushEndpoint)) { + if (debug) { + Log.i(LOG_TAG, "Push configuration autopushEndpoint changed! Was: " + registration.autopushEndpoint + "; now: " + endpoint); + } else { + Log.i(LOG_TAG, "Push configuration autopushEndpoint changed!"); + } + + newRegistration = new PushRegistration(endpoint, debug, Fetched.now(null), null); + + if (registration.uaid.value != null) { + // New endpoint! All registrations and subscriptions have been dropped, and + // should be removed remotely. + unregisterUserAgentOnBackgroundThread(registration); + } + } else if (debug != registration.debug) { + Log.i(LOG_TAG, "Push configuration debug changed: " + debug); + newRegistration = registration.withDebug(debug); + } else { + newRegistration = registration; + } + } else { + if (debug) { + Log.i(LOG_TAG, "Push configuration set: " + endpoint + "; debug: " + debug); + } else { + Log.i(LOG_TAG, "Push configuration set!"); + } + newRegistration = new PushRegistration(endpoint, debug, new Fetched(null, now), null); + } + + if (newRegistration != registration) { + state.putRegistration(profileName, newRegistration); + state.checkpoint(); + } + + return newRegistration; + } + + private void unregisterUserAgentOnBackgroundThread(final PushRegistration registration) { + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + try { + pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug).unregisterUserAgent(registration.uaid.value, registration.secret); + Log.i(LOG_TAG, "Unregistered user agent with uaid: " + registration.uaid.value); + } catch (PushClient.LocalException | AutopushClientException e) { + Log.w(LOG_TAG, "Failed to unregister user agent with uaid; ignoring: " + registration.uaid.value, e); + } + } + }); + } + + protected @NonNull PushRegistration advanceRegistration(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { + final PushRegistration registration = state.getRegistration(profileName); + if (registration == null || registration.autopushEndpoint == null) { + Log.i(LOG_TAG, "Cannot advance to registered: registration needs configuration."); + throw new ProfileNeedsConfigurationException(); + } + return advanceRegistration(registration, profileName, now); + } + + protected @NonNull PushRegistration advanceRegistration(final PushRegistration registration, final @NonNull String profileName, final long now) throws AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException { + final Fetched gcmToken = gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, registration.debug); + + final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug); + + if (registration.uaid.value == null) { + if (registration.debug) { + Log.i(LOG_TAG, "No uaid; requesting from autopush endpoint: " + registration.autopushEndpoint); + } else { + Log.i(LOG_TAG, "No uaid: requesting from autopush endpoint."); + } + final RegisterUserAgentResponse result = pushClient.registerUserAgent(gcmToken.value); + if (registration.debug) { + Log.i(LOG_TAG, "Got uaid: " + result.uaid + " and secret: " + result.secret); + } else { + Log.i(LOG_TAG, "Got uaid and secret."); + } + final long nextNow = System.currentTimeMillis(); + final PushRegistration nextRegistration = registration.withUserAgentID(result.uaid, result.secret, nextNow); + state.putRegistration(profileName, nextRegistration); + state.checkpoint(); + return advanceRegistration(nextRegistration, profileName, nextNow); + } + + if (registration.uaid.timestamp + TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS < now + || registration.uaid.timestamp < gcmToken.timestamp) { + if (registration.debug) { + Log.i(LOG_TAG, "Stale uaid; re-registering with autopush endpoint: " + registration.autopushEndpoint); + } else { + Log.i(LOG_TAG, "Stale uaid: re-registering with autopush endpoint."); + } + + pushClient.reregisterUserAgent(registration.uaid.value, registration.secret, gcmToken.value); + + Log.i(LOG_TAG, "Re-registered uaid and secret."); + final long nextNow = System.currentTimeMillis(); + final PushRegistration nextRegistration = registration.withUserAgentID(registration.uaid.value, registration.secret, nextNow); + state.putRegistration(profileName, nextRegistration); + state.checkpoint(); + return advanceRegistration(nextRegistration, profileName, nextNow); + } + + Log.d(LOG_TAG, "Existing uaid is fresh; no need to request from autopush endpoint."); + return registration; + } + + public void invalidateGcmToken() { + gcmClient.invalidateToken(); + } + + public void startup(long now) { + try { + Log.i(LOG_TAG, "Startup: requesting GCM token."); + gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false); // For side-effects. + } catch (GcmTokenClient.NeedsGooglePlayServicesException e) { + // Requires user intervention. At App startup, we don't want to address this. In + // response to user activity, we do want to try to have the user address this. + Log.w(LOG_TAG, "Startup: needs Google Play Services. Ignoring until GCM is requested in response to user activity."); + return; + } catch (IOException e) { + // We're temporarily unable to get a GCM token. There's nothing to be done; we'll + // try to advance the App's state in response to user activity or at next startup. + Log.w(LOG_TAG, "Startup: Google Play Services is available, but we can't get a token; ignoring.", e); + return; + } + + Log.i(LOG_TAG, "Startup: advancing all registrations."); + final Map<String, PushRegistration> registrations = state.getRegistrations(); + + // Now advance all registrations. + try { + final Iterator<Map.Entry<String, PushRegistration>> it = registrations.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry<String, PushRegistration> entry = it.next(); + final String profileName = entry.getKey(); + final PushRegistration registration = entry.getValue(); + if (registration.subscriptions.isEmpty()) { + Log.i(LOG_TAG, "Startup: no subscriptions for profileName; not advancing registration: " + profileName); + continue; + } + + try { + advanceRegistration(profileName, now); // For side-effects. + Log.i(LOG_TAG, "Startup: advanced registration for profileName: " + profileName); + } catch (ProfileNeedsConfigurationException e) { + Log.i(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; profile needs configuration from Gecko."); + } catch (AutopushClientException e) { + if (e.isTransientError()) { + Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got transient autopush error. Ignoring; will advance on demand.", e); + } else { + Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got permanent autopush error. Removing registration entirely.", e); + it.remove(); + } + } catch (PushClient.LocalException e) { + Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got local exception. Ignoring; will advance on demand.", e); + } + } + } catch (GcmTokenClient.NeedsGooglePlayServicesException e) { + Log.w(LOG_TAG, "Startup: cannot advance any registrations; need Google Play Services!", e); + return; + } catch (IOException e) { + Log.w(LOG_TAG, "Startup: cannot advance any registrations; intermittent Google Play Services exception; ignoring, will advance on demand.", e); + return; + } + + // We may have removed registrations above. Checkpoint just to be safe! + state.checkpoint(); + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push; + +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.gcm.GcmTokenClient; +import org.robolectric.RuntimeEnvironment; + +import java.util.UUID; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(TestRunner.class) +public class TestPushManager { + private PushState state; + private GcmTokenClient gcmTokenClient; + private PushClient pushClient; + private PushManager manager; + + @Before + public void setUp() throws Exception { + state = new PushState(RuntimeEnvironment.application, "test.json"); + gcmTokenClient = mock(GcmTokenClient.class); + doReturn(new Fetched("opaque-gcm-token", System.currentTimeMillis())).when(gcmTokenClient).getToken(anyString(), anyBoolean()); + + // Configure a mock PushClient. + pushClient = mock(PushClient.class); + doReturn(new RegisterUserAgentResponse("opaque-uaid", "opaque-secret")) + .when(pushClient) + .registerUserAgent(anyString()); + + doReturn(new SubscribeChannelResponse("opaque-chid", "https://localhost:8085/opaque-push-endpoint")) + .when(pushClient) + .subscribeChannel(anyString(), anyString()); + + PushManager.PushClientFactory pushClientFactory = mock(PushManager.PushClientFactory.class); + doReturn(pushClient).when(pushClientFactory).getPushClient(anyString(), anyBoolean()); + + manager = new PushManager(state, gcmTokenClient, pushClientFactory); + } + + private void assertOnlyConfigured(PushRegistration registration, String endpoint, boolean debug) { + Assert.assertNotNull(registration); + Assert.assertEquals(registration.autopushEndpoint, endpoint); + Assert.assertEquals(registration.debug, debug); + Assert.assertNull(registration.uaid.value); + } + + private void assertRegistered(PushRegistration registration, String endpoint, boolean debug) { + Assert.assertNotNull(registration); + Assert.assertEquals(registration.autopushEndpoint, endpoint); + Assert.assertEquals(registration.debug, debug); + Assert.assertNotNull(registration.uaid.value); + } + + private void assertSubscribed(PushSubscription subscription) { + Assert.assertNotNull(subscription); + Assert.assertNotNull(subscription.chid); + } + + @Test + public void testConfigure() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8081", false, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8081", false); + + registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8082", true); + } + + @Test(expected=PushManager.ProfileNeedsConfigurationException.class) + public void testRegisterBeforeConfigure() throws Exception { + PushRegistration registration = state.getRegistration("default"); + Assert.assertNull(registration); + + // Trying to register a User Agent fails before configuration. + manager.registerUserAgent("default", System.currentTimeMillis()); + } + + @Test + public void testRegister() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8082", false, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8082", false); + + // Let's register a User Agent, so that we can witness unregistration. + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8082", false); + + // Changing the debug flag should update but not try to unregister the User Agent. + registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8082", true); + + // Changing the configuration endpoint should update and try to unregister the User Agent. + registration = manager.configure("default", "http://localhost:8083", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8083", true); + } + + @Test + public void testRegisterMultipleProfiles() throws Exception { + PushRegistration registration1 = manager.configure("default1", "http://localhost:8081", true, System.currentTimeMillis()); + PushRegistration registration2 = manager.configure("default2", "http://localhost:8082", true, System.currentTimeMillis()); + assertOnlyConfigured(registration1, "http://localhost:8081", true); + assertOnlyConfigured(registration2, "http://localhost:8082", true); + verify(gcmTokenClient, times(0)).getToken(anyString(), anyBoolean()); + + registration1 = manager.registerUserAgent("default1", System.currentTimeMillis()); + assertRegistered(registration1, "http://localhost:8081", true); + + registration2 = manager.registerUserAgent("default2", System.currentTimeMillis()); + assertRegistered(registration2, "http://localhost:8082", true); + + // Just the debug flag should not unregister the User Agent. + registration1 = manager.configure("default1", "http://localhost:8081", false, System.currentTimeMillis()); + assertRegistered(registration1, "http://localhost:8081", false); + + // But the configuration endpoint should unregister the correct User Agent. + registration2 = manager.configure("default2", "http://localhost:8083", false, System.currentTimeMillis()); + } + + @Test + public void testSubscribeChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + // We should be able to register with non-null serviceData. + final JSONObject webpushData = new JSONObject(); + webpushData.put("version", 5); + PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, System.currentTimeMillis()); + assertSubscribed(subscription); + + subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid); + Assert.assertNotNull(subscription); + Assert.assertEquals(5, subscription.serviceData.get("version")); + + // We should be able to register with null serviceData. + subscription = manager.subscribeChannel("default", "sync", null, System.currentTimeMillis()); + assertSubscribed(subscription); + + subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid); + Assert.assertNotNull(subscription); + Assert.assertNull(subscription.serviceData); + } + + @Test + public void testUnsubscribeChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + // We should be able to register with non-null serviceData. + final JSONObject webpushData = new JSONObject(); + webpushData.put("version", 5); + PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, System.currentTimeMillis()); + assertSubscribed(subscription); + + // No exception is success. + manager.unsubscribeChannel(subscription.chid); + } + + public void testUnsubscribeUnknownChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + doThrow(new RuntimeException()) + .when(pushClient) + .unsubscribeChannel(anyString(), anyString(), anyString()); + + // Un-subscribing from an unknown channel succeeds: we just ignore the request. + manager.unsubscribeChannel(UUID.randomUUID().toString()); + } + + @Test + public void testStartupBeforeConfiguration() throws Exception { + verify(gcmTokenClient, never()).getToken(anyString(), anyBoolean()); + manager.startup(System.currentTimeMillis()); + verify(gcmTokenClient, times(1)).getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false); + } + + @Test + public void testStartupBeforeRegistration() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + manager.startup(System.currentTimeMillis()); + verify(gcmTokenClient, times(1)).getToken(anyString(), anyBoolean()); + } + + @Test + public void testStartupAfterRegistration() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", true); + + manager.startup(System.currentTimeMillis()); + + // Rather tautological. + PushRegistration updatedRegistration = manager.state.getRegistration("default"); + Assert.assertEquals(registration.uaid, updatedRegistration.uaid); + } + + @Test + public void testStartupAfterSubscription() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", true); + + PushSubscription subscription = manager.subscribeChannel("default", "webpush", null, System.currentTimeMillis()); + assertSubscribed(subscription); + + manager.startup(System.currentTimeMillis()); + + // Rather tautological. + registration = manager.registrationForSubscription(subscription.chid); + PushSubscription updatedSubscription = registration.getSubscription(subscription.chid); + Assert.assertEquals(subscription.chid, updatedSubscription.chid); + } +}