mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 15:28:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
71
mobile/android/base/java/org/mozilla/gecko/push/Fetched.java
Normal file
71
mobile/android/base/java/org/mozilla/gecko/push/Fetched.java
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* -*- 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 org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Pair a (String) value with a timestamp. The timestamp is usually when the
|
||||
* value was fetched from a remote service or when the value was locally
|
||||
* generated.
|
||||
*
|
||||
* It's awkward to serialize generic values to JSON -- that requires lots of
|
||||
* factory classes -- so we specialize to String instances.
|
||||
*/
|
||||
public class Fetched {
|
||||
public final String value;
|
||||
public final long timestamp;
|
||||
|
||||
public Fetched(String value, long timestamp) {
|
||||
this.value = value;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static Fetched now(String value) {
|
||||
return new Fetched(value, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static @NonNull Fetched fromJSONObject(@NonNull JSONObject json) {
|
||||
final String value = json.optString("value", null);
|
||||
final String timestampString = json.optString("timestamp", null);
|
||||
final long timestamp = timestampString != null ? Long.valueOf(timestampString) : 0L;
|
||||
return new Fetched(value, timestamp);
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
final JSONObject jsonObject = new JSONObject();
|
||||
if (value != null) {
|
||||
jsonObject.put("value", value);
|
||||
} else {
|
||||
jsonObject.remove("value");
|
||||
}
|
||||
jsonObject.put("timestamp", Long.toString(timestamp));
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
// Auto-generated.
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Fetched fetched = (Fetched) o;
|
||||
|
||||
if (timestamp != fetched.timestamp) return false;
|
||||
return !(value != null ? !value.equals(fetched.value) : fetched.value != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Auto-generated.
|
||||
int result = value != null ? value.hashCode() : 0;
|
||||
result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
110
mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
Normal file
110
mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/* -*- 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 android.support.annotation.Nullable;
|
||||
|
||||
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, @Nullable String appServerKey) throws LocalException, AutopushClientException {
|
||||
final Delegate<SubscribeChannelResponse> delegate = new Delegate<>();
|
||||
autopushClient.subscribeChannel(uaid, secret, appServerKey, 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.
|
||||
}
|
||||
}
|
||||
354
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
Normal file
354
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
/* -*- 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.support.annotation.Nullable;
|
||||
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.emptyMap();
|
||||
}
|
||||
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, @Nullable String appServerKey, 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, appServerKey, System.currentTimeMillis());
|
||||
return subscription;
|
||||
}
|
||||
|
||||
protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, 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, appServerKey);
|
||||
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,126 @@
|
|||
/* -*- 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 android.support.annotation.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represent an autopush User Agent registration.
|
||||
* <p/>
|
||||
* Such a registration associates an endpoint, optional debug flag, some Google
|
||||
* Cloud Messaging data, and the returned uaid and secret.
|
||||
* <p/>
|
||||
* Each registration is associated to a single Gecko profile, although we don't
|
||||
* enforce that here. This class is immutable, so it is by definition
|
||||
* thread-safe.
|
||||
*/
|
||||
public class PushRegistration {
|
||||
public final String autopushEndpoint;
|
||||
public final boolean debug;
|
||||
// TODO: fold (timestamp, {uaid, secret}) into this class.
|
||||
public final @NonNull Fetched uaid;
|
||||
public final String secret;
|
||||
|
||||
protected final @NonNull Map<String, PushSubscription> subscriptions;
|
||||
|
||||
public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret, @NonNull Map<String, PushSubscription> subscriptions) {
|
||||
this.autopushEndpoint = autopushEndpoint;
|
||||
this.debug = debug;
|
||||
this.uaid = uaid;
|
||||
this.secret = secret;
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret) {
|
||||
this(autopushEndpoint, debug, uaid, secret, new HashMap<String, PushSubscription>());
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
final JSONObject subscriptions = new JSONObject();
|
||||
for (Map.Entry<String, PushSubscription> entry : this.subscriptions.entrySet()) {
|
||||
subscriptions.put(entry.getKey(), entry.getValue().toJSONObject());
|
||||
}
|
||||
|
||||
final JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("autopushEndpoint", autopushEndpoint);
|
||||
jsonObject.put("debug", debug);
|
||||
jsonObject.put("uaid", uaid.toJSONObject());
|
||||
jsonObject.put("secret", secret);
|
||||
jsonObject.put("subscriptions", subscriptions);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
public static PushRegistration fromJSONObject(@NonNull JSONObject registration) throws JSONException {
|
||||
final String endpoint = registration.optString("autopushEndpoint", null);
|
||||
final boolean debug = registration.getBoolean("debug");
|
||||
final Fetched uaid = Fetched.fromJSONObject(registration.getJSONObject("uaid"));
|
||||
final String secret = registration.optString("secret", null);
|
||||
|
||||
final JSONObject subscriptionsObject = registration.getJSONObject("subscriptions");
|
||||
final Map<String, PushSubscription> subscriptions = new HashMap<>();
|
||||
final Iterator<String> it = subscriptionsObject.keys();
|
||||
while (it.hasNext()) {
|
||||
final String chid = it.next();
|
||||
subscriptions.put(chid, PushSubscription.fromJSONObject(subscriptionsObject.getJSONObject(chid)));
|
||||
}
|
||||
|
||||
return new PushRegistration(endpoint, debug, uaid, secret, subscriptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
// Auto-generated.
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PushRegistration that = (PushRegistration) o;
|
||||
|
||||
if (autopushEndpoint != null ? !autopushEndpoint.equals(that.autopushEndpoint) : that.autopushEndpoint != null)
|
||||
return false;
|
||||
if (!uaid.equals(that.uaid)) return false;
|
||||
if (secret != null ? !secret.equals(that.secret) : that.secret != null) return false;
|
||||
if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) return false;
|
||||
return (debug == that.debug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Auto-generated.
|
||||
int result = autopushEndpoint != null ? autopushEndpoint.hashCode() : 0;
|
||||
result = 31 * result + (debug ? 1 : 0);
|
||||
result = 31 * result + uaid.hashCode();
|
||||
result = 31 * result + (secret != null ? secret.hashCode() : 0);
|
||||
result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public PushRegistration withDebug(boolean debug) {
|
||||
return new PushRegistration(this.autopushEndpoint, debug, this.uaid, this.secret, this.subscriptions);
|
||||
}
|
||||
|
||||
public PushRegistration withUserAgentID(String uaid, String secret, long nextNow) {
|
||||
return new PushRegistration(this.autopushEndpoint, this.debug, new Fetched(uaid, nextNow), secret, this.subscriptions);
|
||||
}
|
||||
|
||||
public PushSubscription getSubscription(@NonNull String chid) {
|
||||
return subscriptions.get(chid);
|
||||
}
|
||||
|
||||
public PushSubscription putSubscription(@NonNull String chid, @NonNull PushSubscription subscription) {
|
||||
return subscriptions.put(chid, subscription);
|
||||
}
|
||||
|
||||
public PushSubscription removeSubscription(@NonNull String chid) {
|
||||
return subscriptions.remove(chid);
|
||||
}
|
||||
}
|
||||
460
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
Normal file
460
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
/* -*- 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.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.GeckoService;
|
||||
import org.mozilla.gecko.GeckoThread;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.annotation.ReflectionTarget;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.fxa.FxAccountPushHandler;
|
||||
import org.mozilla.gecko.gcm.GcmTokenClient;
|
||||
import org.mozilla.gecko.push.autopush.AutopushClientException;
|
||||
import org.mozilla.gecko.util.BundleEventListener;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class that handles messages used in the Google Cloud Messaging and DOM push API integration.
|
||||
* <p/>
|
||||
* This singleton services Gecko messages from dom/push/PushServiceAndroidGCM.jsm and Google Cloud
|
||||
* Messaging requests.
|
||||
* <p/>
|
||||
* It is expected that Gecko is started (if not already running) soon after receiving GCM messages
|
||||
* otherwise there is a greater risk that pending messages that have not been handle by Gecko will
|
||||
* be lost if this service is killed.
|
||||
* <p/>
|
||||
* It's worth noting that we allow the DOM push API in restricted profiles.
|
||||
*/
|
||||
@ReflectionTarget
|
||||
public class PushService implements BundleEventListener {
|
||||
private static final String LOG_TAG = "GeckoPushService";
|
||||
|
||||
public static final String SERVICE_WEBPUSH = "webpush";
|
||||
public static final String SERVICE_FXA = "fxa";
|
||||
|
||||
private static PushService sInstance;
|
||||
|
||||
private static final String[] GECKO_EVENTS = new String[] {
|
||||
"PushServiceAndroidGCM:Configure",
|
||||
"PushServiceAndroidGCM:DumpRegistration",
|
||||
"PushServiceAndroidGCM:DumpSubscriptions",
|
||||
"PushServiceAndroidGCM:Initialized",
|
||||
"PushServiceAndroidGCM:Uninitialized",
|
||||
"PushServiceAndroidGCM:RegisterUserAgent",
|
||||
"PushServiceAndroidGCM:UnregisterUserAgent",
|
||||
"PushServiceAndroidGCM:SubscribeChannel",
|
||||
"PushServiceAndroidGCM:UnsubscribeChannel",
|
||||
"FxAccountsPush:Initialized",
|
||||
"FxAccountsPush:ReceivedPushMessageToDecode:Response",
|
||||
"History:GetPrePathLastVisitedTimeMilliseconds",
|
||||
};
|
||||
|
||||
private enum GeckoComponent {
|
||||
FxAccountsPush,
|
||||
PushServiceAndroidGCM
|
||||
}
|
||||
|
||||
public static synchronized PushService getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
onCreate(context);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@ReflectionTarget
|
||||
public static synchronized void onCreate(Context context) {
|
||||
if (sInstance != null) {
|
||||
return;
|
||||
}
|
||||
sInstance = new PushService(context);
|
||||
|
||||
sInstance.registerGeckoEventListener();
|
||||
sInstance.onStartup();
|
||||
}
|
||||
|
||||
protected final PushManager pushManager;
|
||||
|
||||
// NB: These are not thread-safe, we're depending on these being access from the same background thread.
|
||||
private boolean isReadyPushServiceAndroidGCM = false;
|
||||
private boolean isReadyFxAccountsPush = false;
|
||||
private final List<JSONObject> pendingPushMessages;
|
||||
|
||||
public PushService(Context context) {
|
||||
pushManager = new PushManager(new PushState(context, "GeckoPushState.json"), new GcmTokenClient(context), new PushManager.PushClientFactory() {
|
||||
@Override
|
||||
public PushClient getPushClient(String autopushEndpoint, boolean debug) {
|
||||
return new PushClient(autopushEndpoint);
|
||||
}
|
||||
});
|
||||
|
||||
pendingPushMessages = new LinkedList<>();
|
||||
}
|
||||
|
||||
public void onStartup() {
|
||||
Log.i(LOG_TAG, "Starting up.");
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
|
||||
try {
|
||||
pushManager.startup(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void onRefresh() {
|
||||
Log.i(LOG_TAG, "Google Play Services requested GCM token refresh; invalidating GCM token and running startup again.");
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
|
||||
pushManager.invalidateGcmToken();
|
||||
try {
|
||||
pushManager.startup(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Got exception during refresh; ignoring.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void onMessageReceived(final @NonNull Context context, final @NonNull Bundle bundle) {
|
||||
Log.i(LOG_TAG, "Google Play Services GCM message received; delivering.");
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
|
||||
final String chid = bundle.getString("chid");
|
||||
if (chid == null) {
|
||||
Log.w(LOG_TAG, "No chid found; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
final PushRegistration registration = pushManager.registrationForSubscription(chid);
|
||||
if (registration == null) {
|
||||
Log.w(LOG_TAG, "Cannot find registration corresponding to subscription for chid: " + chid + "; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
final PushSubscription subscription = registration.getSubscription(chid);
|
||||
if (subscription == null) {
|
||||
// This should never happen. There's not much to be done; in the future, perhaps we
|
||||
// could try to drop the remote subscription?
|
||||
Log.e(LOG_TAG, "No subscription found for chid: " + chid + "; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isWebPush = SERVICE_WEBPUSH.equals(subscription.service);
|
||||
boolean isFxAPush = SERVICE_FXA.equals(subscription.service);
|
||||
if (!isWebPush && !isFxAPush) {
|
||||
Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
|
||||
|
||||
if (subscription.serviceData == null) {
|
||||
Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
|
||||
return;
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.SERVICE, "dom-push-api");
|
||||
|
||||
final String profileName = subscription.serviceData.optString("profileName", null);
|
||||
final String profilePath = subscription.serviceData.optString("profilePath", null);
|
||||
if (profileName == null || profilePath == null) {
|
||||
Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (canSendPushMessagesToGecko()) {
|
||||
if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
|
||||
Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
|
||||
GeckoService.setIntentProfile(intent, profileName, profilePath);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
final JSONObject data = new JSONObject();
|
||||
try {
|
||||
data.put("channelID", chid);
|
||||
data.put("con", bundle.getString("con"));
|
||||
data.put("enc", bundle.getString("enc"));
|
||||
// Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
|
||||
// Gecko handler will verify this.
|
||||
data.put("cryptokey", bundle.getString("cryptokey"));
|
||||
data.put("enckey", bundle.getString("enckey"));
|
||||
data.put("message", bundle.getString("body"));
|
||||
|
||||
if (!canSendPushMessagesToGecko()) {
|
||||
data.put("profileName", profileName);
|
||||
data.put("profilePath", profilePath);
|
||||
data.put("service", subscription.service);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canSendPushMessagesToGecko()) {
|
||||
Log.i(LOG_TAG, "Required service not initialized, adding message to queue.");
|
||||
pendingPushMessages.add(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWebPush) {
|
||||
sendMessageToGeckoService(data);
|
||||
} else {
|
||||
sendMessageToDecodeToGeckoService(data);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void sendMessageToGeckoService(final @NonNull JSONObject message) {
|
||||
Log.i(LOG_TAG, "Delivering dom/push message to Gecko!");
|
||||
GeckoAppShell.notifyObservers("PushServiceAndroidGCM:ReceivedPushMessage",
|
||||
message.toString(),
|
||||
GeckoThread.State.PROFILE_READY);
|
||||
}
|
||||
|
||||
protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) {
|
||||
Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!");
|
||||
GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode",
|
||||
message.toString(),
|
||||
GeckoThread.State.PROFILE_READY);
|
||||
}
|
||||
|
||||
protected void registerGeckoEventListener() {
|
||||
Log.d(LOG_TAG, "Registered Gecko event listener.");
|
||||
EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS);
|
||||
}
|
||||
|
||||
protected void unregisterGeckoEventListener() {
|
||||
Log.d(LOG_TAG, "Unregistered Gecko event listener.");
|
||||
EventDispatcher.getInstance().unregisterBackgroundThreadListener(this, GECKO_EVENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final Bundle message, final EventCallback callback) {
|
||||
Log.i(LOG_TAG, "Handling event: " + event);
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
|
||||
final Context context = GeckoAppShell.getApplicationContext();
|
||||
// We're invoked in response to a Gecko message on a background thread. We should always
|
||||
// be able to safely retrieve the current Gecko profile.
|
||||
final GeckoProfile geckoProfile = GeckoProfile.get(context);
|
||||
|
||||
if (callback == null) {
|
||||
Log.e(LOG_TAG, "callback must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ("PushServiceAndroidGCM:Initialized".equals(event)) {
|
||||
processComponentState(GeckoComponent.PushServiceAndroidGCM, true);
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:Uninitialized".equals(event)) {
|
||||
processComponentState(GeckoComponent.PushServiceAndroidGCM, false);
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("FxAccountsPush:Initialized".equals(event)) {
|
||||
processComponentState(GeckoComponent.FxAccountsPush, true);
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:Configure".equals(event)) {
|
||||
final String endpoint = message.getString("endpoint");
|
||||
if (endpoint == null) {
|
||||
callback.sendError("endpoint must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
final boolean debug = message.getBoolean("debug", false);
|
||||
pushManager.configure(geckoProfile.getName(), endpoint, debug, System.currentTimeMillis()); // For side effects.
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:DumpRegistration".equals(event)) {
|
||||
// In the future, this might be used to interrogate the Java Push Manager
|
||||
// registration state from JavaScript.
|
||||
callback.sendError("Not yet implemented!");
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:DumpSubscriptions".equals(event)) {
|
||||
try {
|
||||
final Map<String, PushSubscription> result = pushManager.allSubscriptionsForProfile(geckoProfile.getName());
|
||||
|
||||
final JSONObject json = new JSONObject();
|
||||
for (Map.Entry<String, PushSubscription> entry : result.entrySet()) {
|
||||
json.put(entry.getKey(), entry.getValue().toJSONObject());
|
||||
}
|
||||
callback.sendSuccess(json);
|
||||
} catch (JSONException e) {
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:RegisterUserAgent".equals(event)) {
|
||||
try {
|
||||
pushManager.registerUserAgent(geckoProfile.getName(), System.currentTimeMillis()); // For side-effects.
|
||||
callback.sendSuccess(null);
|
||||
} catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:UnregisterUserAgent".equals(event)) {
|
||||
// In the future, this might be used to tell the Java Push Manager to unregister
|
||||
// a User Agent entirely from JavaScript. Right now, however, everything is
|
||||
// subscription based; there's no concept of unregistering all subscriptions
|
||||
// simultaneously.
|
||||
callback.sendError("Not yet implemented!");
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) {
|
||||
final String service = SERVICE_FXA.equals(message.getString("service")) ?
|
||||
SERVICE_FXA :
|
||||
SERVICE_WEBPUSH;
|
||||
final JSONObject serviceData;
|
||||
final String appServerKey = message.getString("appServerKey");
|
||||
try {
|
||||
serviceData = new JSONObject();
|
||||
serviceData.put("profileName", geckoProfile.getName());
|
||||
serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
final PushSubscription subscription;
|
||||
try {
|
||||
subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, appServerKey, System.currentTimeMillis());
|
||||
} catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("channelID", subscription.chid);
|
||||
json.put("endpoint", subscription.webpushEndpoint);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "dom-push-api");
|
||||
callback.sendSuccess(json);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:UnsubscribeChannel".equals(event)) {
|
||||
final String channelID = message.getString("channelID");
|
||||
if (channelID == null) {
|
||||
callback.sendError("channelID must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire and forget. See comments in the function itself.
|
||||
final PushSubscription pushSubscription = pushManager.unsubscribeChannel(channelID);
|
||||
if (pushSubscription != null) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "dom-push-api");
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback.sendError("Could not unsubscribe from channel: " + channelID);
|
||||
return;
|
||||
}
|
||||
if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
|
||||
FxAccountPushHandler.handleFxAPushMessage(context, message);
|
||||
return;
|
||||
}
|
||||
if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
|
||||
if (callback == null) {
|
||||
Log.e(LOG_TAG, "callback must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
final String prePath = message.getString("prePath");
|
||||
if (prePath == null) {
|
||||
callback.sendError("prePath must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
// We're on a background thread, so we can be synchronous.
|
||||
final long millis = BrowserDB.from(geckoProfile).getPrePathLastVisitedTimeMilliseconds(
|
||||
context.getContentResolver(), prePath);
|
||||
callback.sendSuccess(millis);
|
||||
return;
|
||||
}
|
||||
} catch (GcmTokenClient.NeedsGooglePlayServicesException e) {
|
||||
// TODO: improve this. Can we find a point where the user is *definitely* interacting
|
||||
// with the WebPush? Perhaps we can show a dialog when interacting with the Push
|
||||
// permissions, and then be more aggressive showing this notification when we have
|
||||
// registrations and subscriptions that can't be advanced.
|
||||
callback.sendError("To handle event [" + event + "], user interaction is needed to enable Google Play Services.");
|
||||
}
|
||||
}
|
||||
|
||||
private void processComponentState(@NonNull GeckoComponent component, boolean isReady) {
|
||||
if (component == GeckoComponent.FxAccountsPush) {
|
||||
isReadyFxAccountsPush = isReady;
|
||||
|
||||
} else if (component == GeckoComponent.PushServiceAndroidGCM) {
|
||||
isReadyPushServiceAndroidGCM = isReady;
|
||||
}
|
||||
|
||||
// Send all pending messages to Gecko.
|
||||
if (canSendPushMessagesToGecko()) {
|
||||
sendPushMessagesToGecko(pendingPushMessages);
|
||||
pendingPushMessages.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canSendPushMessagesToGecko() {
|
||||
return isReadyFxAccountsPush && isReadyPushServiceAndroidGCM;
|
||||
}
|
||||
|
||||
private static void sendPushMessagesToGecko(@NonNull List<JSONObject> messages) {
|
||||
for (JSONObject pushMessage : messages) {
|
||||
final String profileName = pushMessage.optString("profileName", null);
|
||||
final String profilePath = pushMessage.optString("profilePath", null);
|
||||
final String service = pushMessage.optString("service", null);
|
||||
if (profileName == null || profilePath == null ||
|
||||
!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
|
||||
Log.e(LOG_TAG, "Mismatched profile for chid: " +
|
||||
pushMessage.optString("channelID") +
|
||||
"; ignoring dom/push message.");
|
||||
continue;
|
||||
}
|
||||
if (SERVICE_WEBPUSH.equals(service)) {
|
||||
sendMessageToGeckoService(pushMessage);
|
||||
} else if (SERVICE_FXA.equals(service)) {
|
||||
sendMessageToDecodeToGeckoService(pushMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
mobile/android/base/java/org/mozilla/gecko/push/PushState.java
Normal file
137
mobile/android/base/java/org/mozilla/gecko/push/PushState.java
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/* -*- 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.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.support.v4.util.AtomicFile;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Firefox for Android maintains an App-wide mapping associating
|
||||
* profile names to push registrations. Each push registration in turn associates channels to
|
||||
* push subscriptions.
|
||||
* <p/>
|
||||
* We use a simple storage model of JSON backed by an atomic file. It is assumed that instances
|
||||
* of this class will reference distinct files on disk; and that all accesses will be happen on a
|
||||
* single (worker thread).
|
||||
*/
|
||||
public class PushState {
|
||||
private static final String LOG_TAG = "GeckoPushState";
|
||||
|
||||
private static final long VERSION = 1L;
|
||||
|
||||
protected final @NonNull AtomicFile file;
|
||||
|
||||
protected final @NonNull Map<String, PushRegistration> registrations;
|
||||
|
||||
public PushState(Context context, @NonNull String fileName) {
|
||||
this.registrations = new HashMap<>();
|
||||
|
||||
file = new AtomicFile(new File(context.getApplicationInfo().dataDir, fileName));
|
||||
synchronized (file) {
|
||||
try {
|
||||
final String s = new String(file.readFully(), "UTF-8");
|
||||
final JSONObject temp = new JSONObject(s);
|
||||
if (temp.optLong("version", 0L) != VERSION) {
|
||||
throw new JSONException("Unknown version!");
|
||||
}
|
||||
|
||||
final JSONObject registrationsObject = temp.getJSONObject("registrations");
|
||||
final Iterator<String> it = registrationsObject.keys();
|
||||
while (it.hasNext()) {
|
||||
final String profileName = it.next();
|
||||
final PushRegistration registration = PushRegistration.fromJSONObject(registrationsObject.getJSONObject(profileName));
|
||||
this.registrations.put(profileName, registration);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.i(LOG_TAG, "No storage found; starting fresh.");
|
||||
this.registrations.clear();
|
||||
} catch (IOException | JSONException e) {
|
||||
Log.w(LOG_TAG, "Got exception reading storage; dropping storage and starting fresh.", e);
|
||||
this.registrations.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
final JSONObject registrations = new JSONObject();
|
||||
for (Map.Entry<String, PushRegistration> entry : this.registrations.entrySet()) {
|
||||
registrations.put(entry.getKey(), entry.getValue().toJSONObject());
|
||||
}
|
||||
|
||||
final JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("version", 1L);
|
||||
jsonObject.put("registrations", registrations);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously persist the cache to disk.
|
||||
* @return whether the cache was persisted successfully.
|
||||
*/
|
||||
@WorkerThread
|
||||
public boolean checkpoint() {
|
||||
synchronized (file) {
|
||||
FileOutputStream fileOutputStream = null;
|
||||
try {
|
||||
fileOutputStream = file.startWrite();
|
||||
fileOutputStream.write(toJSONObject().toString().getBytes("UTF-8"));
|
||||
file.finishWrite(fileOutputStream);
|
||||
return true;
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.e(LOG_TAG, "Got exception writing JSON storage; ignoring.", e);
|
||||
if (fileOutputStream != null) {
|
||||
file.failWrite(fileOutputStream);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PushRegistration putRegistration(@NonNull String profileName, @NonNull PushRegistration registration) {
|
||||
return registrations.put(profileName, registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the existing push registration for the given profile name.
|
||||
* @return the push registration, if one is registered; null otherwise.
|
||||
*/
|
||||
public PushRegistration getRegistration(@NonNull String profileName) {
|
||||
return registrations.get(profileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all push registrations, keyed by profile names.
|
||||
* @return a map of all push registrations. <b>The map is intentionally mutable - be careful!</b>
|
||||
*/
|
||||
public @NonNull Map<String, PushRegistration> getRegistrations() {
|
||||
return registrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any existing push registration for the given profile name.
|
||||
* </p>
|
||||
* Most registration removals are during iteration, which should use an iterator that is
|
||||
* aware of removals.
|
||||
* @return the removed push registration, if one was removed; null otherwise.
|
||||
*/
|
||||
public PushRegistration removeRegistration(@NonNull String profileName) {
|
||||
return registrations.remove(profileName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/* -*- 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.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represent an autopush Channel subscription.
|
||||
* <p/>
|
||||
* Such a subscription associates a user agent and autopush data with a channel
|
||||
* ID, a WebPush endpoint, and some service-specific data.
|
||||
* <p/>
|
||||
* Cloud Messaging data, and the returned uaid and secret.
|
||||
* <p/>
|
||||
* Each registration is associated to a single Gecko profile, although we don't
|
||||
* enforce that here. This class is immutable, so it is by definition
|
||||
* thread-safe.
|
||||
*/
|
||||
public class PushSubscription {
|
||||
public final @NonNull String chid;
|
||||
public final @NonNull String profileName;
|
||||
public final @NonNull String webpushEndpoint;
|
||||
public final @NonNull String service;
|
||||
public final JSONObject serviceData;
|
||||
|
||||
public PushSubscription(@NonNull String chid, @NonNull String profileName, @NonNull String webpushEndpoint, @NonNull String service, JSONObject serviceData) {
|
||||
this.chid = chid;
|
||||
this.profileName = profileName;
|
||||
this.webpushEndpoint = webpushEndpoint;
|
||||
this.service = service;
|
||||
this.serviceData = serviceData;
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
final JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("chid", chid);
|
||||
jsonObject.put("profileName", profileName);
|
||||
jsonObject.put("webpushEndpoint", webpushEndpoint);
|
||||
jsonObject.put("service", service);
|
||||
jsonObject.put("serviceData", serviceData);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
public static PushSubscription fromJSONObject(@NonNull JSONObject subscription) throws JSONException {
|
||||
final String chid = subscription.getString("chid");
|
||||
final String profileName = subscription.getString("profileName");
|
||||
final String webpushEndpoint = subscription.getString("webpushEndpoint");
|
||||
final String service = subscription.getString("service");
|
||||
final JSONObject serviceData = subscription.optJSONObject("serviceData");
|
||||
return new PushSubscription(chid, profileName, webpushEndpoint, service, serviceData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
// Auto-generated.
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PushSubscription that = (PushSubscription) o;
|
||||
|
||||
if (!chid.equals(that.chid)) return false;
|
||||
if (!profileName.equals(that.profileName)) return false;
|
||||
if (!webpushEndpoint.equals(that.webpushEndpoint)) return false;
|
||||
return service.equals(that.service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Auto-generated.
|
||||
int result = profileName.hashCode();
|
||||
result = 31 * result + chid.hashCode();
|
||||
result = 31 * result + webpushEndpoint.hashCode();
|
||||
result = 31 * result + service.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue