mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 00:05:36 +00:00
Bug 1207714 - Part 3: Implement push manager. r=rnewman
MozReview-Commit-ID: LkUaGFA6YlF --HG-- extra : rebase_source : 0ca118abbe831b931531706c468c84b0d3d5ad5d extra : histedit_source : 53b8a46c84c78db51880053b673ed848f00a0867%2C01aa2e7d5a3fded0ce3d90bb8ad6a7387fd7f7e2
This commit is contained in:
parent
34e1615012
commit
baaa8b439e
109
mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
Normal file
109
mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
Normal file
@ -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.
|
||||
}
|
||||
}
|
353
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
Normal file
353
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user