pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View 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;
}
}

View 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.
}
}

View 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();
}
}

View file

@ -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);
}
}

View 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);
}
}
}
}

View 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);
}
}

View file

@ -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;
}
}