Bug 1287643 - POC registration.
MozReview-Commit-ID: BYFMeQNgumu
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/FxAccountPush.js
@@ -0,0 +1,112 @@
+/* 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/. */
+"use strict";
+
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+const LOG_TAG = "FxAccountPush";
+
+function urlsafeBase64Encode(key) {
+ return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
+}
+
+var FxAccountPush = {
+ pushService: Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService),
+
+ registerPushEndpoint(responseGUID) {
+ Log.i(LOG_TAG, "FxAccountsPush registerPushEndpoint");
+
+ return new Promise((resolve, reject) => {
+ this.pushService.subscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ Log.d(LOG_TAG, "FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ Log.w(LOG_TAG, "FxAccountsPush failed to subscribe", result);
+ reject(new Error("FxAccountsPush failed to subscribe"));
+ }
+ });
+ })
+ .then(subscription => {
+ Messaging.sendRequest({
+ __guid__: responseGUID,
+ type: "FxAccountPush:Subscribe:Response",
+ status: "success",
+ response: {
+ pushCallback: subscription.endpoint,
+ pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
+ pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
+ }
+ });
+ })
+ .catch(err => {
+ Messaging.sendRequest({
+ __guid__: responseGUID,
+ type: "FxAccountPush:Subscribe:Response",
+ status: "failure"
+ });
+ });
+ },
+
+ observe(subject, topic, data) {
+ Log.i(LOG_TAG, `observed topic=${topic}, data=${data}, subject=${subject}`);
+ switch (topic) {
+ case "FxAccountPush:Subscribe":
+ this.registerPushEndpoint(JSON.parse(data).__guid__);
+ break;
+ case this.pushService.pushTopic:
+ if (data === FXA_PUSH_SCOPE) {
+ let message = subject.QueryInterface(Ci.nsIPushMessage);
+ this._onPushMessage(message);
+ }
+ break;
+ case this.pushService.subscriptionChangeTopic:
+ if (data === FXA_PUSH_SCOPE) {
+ this._onPushSubscriptionChange();
+ }
+ break;
+ case "FxAccountPush:Unsubscribe":
+ this.unsubscribe().catch(err => {
+ Log.e(LOG_TAG, "Error during unsubscribe", err);
+ });
+ break;
+ default:
+ break;
+ }
+ },
+
+ _onPushMessage(message) {
+ Log.i(LOG_TAG, "FxAccountPush _onPushMessage");
+ let payload = message.data ? message.data.json() : null;
+ Services.androidBridge.handleFxAPushMessage(payload);
+ },
+
+ _onPushSubscriptionChange() {
+ Log.i(LOG_TAG, "FxAccountPush _onPushSubscriptionChange");
+ Messaging.sendRequest({
+ type: "FxAccountPush:SubscriptionChange",
+ });
+ },
+
+ unsubscribe() {
+ Log.i(LOG_TAG, "FxAccountPush unsubscribe");
+ return new Promise((resolve) => {
+ this.pushService.unsubscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ Log.d(LOG_TAG, "FxAccountPush unsubscribed");
+ } else {
+ Log.d(LOG_TAG, "FxAccountPush had no subscription to unsubscribe");
+ }
+ } else {
+ Log.w(LOG_TAG, "FxAccountPush failed to unsubscribe", result);
+ }
+ return resolve(ok);
+ })
+ })
+ },
+};
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -153,16 +153,17 @@ var lazilyLoadedObserverScripts = [
["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
+ ["FxAccountPush", ["FxAccountPush:Subscribe", "FxAccountPush:Unsubscribe", /* OBSERVER_TOPIC_PUSH */ "push-message", /* OBSERVER_TOPIC_SUBSCRIPTION_CHANGE */ "push-subscription-change"], "chrome://browser/content/FxAccountPush.js"],
];
lazilyLoadedObserverScripts.push(
["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
"chrome://browser/content/ActionBarHandler.js"]
);
if (AppConstants.MOZ_WEBRTC) {
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -30,16 +30,17 @@ chrome.jar:
content/geckoview.js (content/geckoview.js)
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
content/bindings/settings.xml (content/bindings/settings.xml)
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
content/SelectionHandler.js (content/SelectionHandler.js)
content/ActionBarHandler.js (content/ActionBarHandler.js)
content/EmbedRT.js (content/EmbedRT.js)
+ content/FxAccountPush.js (content/FxAccountPush.js)
content/InputWidgetHelper.js (content/InputWidgetHelper.js)
content/WebrtcUI.js (content/WebrtcUI.js)
content/MemoryObserver.js (content/MemoryObserver.js)
content/ConsoleAPI.js (content/ConsoleAPI.js)
content/PluginHelper.js (content/PluginHelper.js)
content/PrintHelper.js (content/PrintHelper.js)
content/OfflineApps.js (content/OfflineApps.js)
content/MasterPassword.js (content/MasterPassword.js)
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -1,34 +1,44 @@
/* 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.fxa;
import android.content.Context;
+import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.TokensAndKeysState;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/* This class provides a way to register the current device against FxA
* and also stores the registration details in the Android FxAccount.
* This should be used in a state where we possess a sessionToken, most likely the Married state.
*/
public class FxAccountDeviceRegistrator {
@@ -63,66 +73,85 @@ public class FxAccountDeviceRegistrator
/**
* @throws InvalidFxAState thrown if we're not in a fxa state with a session token
*/
public static void register(final AndroidFxAccount fxAccount, final Context context,
final RegisterDelegate delegate) throws InvalidFxAState {
final byte[] sessionToken = getSessionToken(fxAccount);
- final FxAccountDevice device;
- String deviceId = fxAccount.getDeviceId();
- String clientName = getClientName(fxAccount, context);
- if (TextUtils.isEmpty(deviceId)) {
- Log.i(LOG_TAG, "Attempting registration for a new device");
- device = FxAccountDevice.forRegister(clientName, "mobile");
- } else {
- Log.i(LOG_TAG, "Attempting registration for an existing device");
- Logger.pii(LOG_TAG, "Device ID: " + deviceId);
- device = FxAccountDevice.forUpdate(deviceId, clientName);
- }
+ JSONObject data = new JSONObject();
+
+ FxAMessaging fxAMessaging = new FxAMessaging();
+
+ fxAMessaging.sendRequestForResult("FxAccountPush:Subscribe", data, new FxAMessaging.EventResponseDelegate() {
+ @Override
+ public void onSuccess(Bundle subscription) {
+ String pushCallback = subscription.getString("pushCallback");
+ String pushPublicKey = subscription.getString("pushPublicKey");
+ String pushAuthKey = subscription.getString("pushAuthKey");
+
+ final FxAccountDevice device;
+ String deviceId = fxAccount.getDeviceId();
+ String clientName = getClientName(fxAccount, context);
+ if (TextUtils.isEmpty(deviceId)) {
+ Log.i(LOG_TAG, "Attempting registration for a new device");
+ device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
+ } else {
+ Log.i(LOG_TAG, "Attempting registration for an existing device");
+ Logger.pii(LOG_TAG, "Device ID: " + deviceId);
+ device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey);
+ }
- ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
- final FxAccountClient20 fxAccountClient =
- new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
- fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
- @Override
- public void handleError(Exception e) {
- Log.e(LOG_TAG, "Error while updating a device registration: ", e);
- delegate.onComplete(null);
+ ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
+ final FxAccountClient20 fxAccountClient =
+ new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
+ @Override
+ public void handleError(Exception e) {
+ Log.e(LOG_TAG, "Error while updating a device registration: ", e);
+ delegate.onComplete(null);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException error) {
+ Log.e(LOG_TAG, "Error while updating a device registration: ", error);
+ if (error.httpStatusCode == 400) {
+ if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
+ recoverFromUnknownDevice(fxAccount);
+ delegate.onComplete(null);
+ } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
+ recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount,
+ context, delegate); // Will call delegate.onComplete
+ }
+ } else
+ if (error.httpStatusCode == 401
+ && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
+ handleTokenError(error, fxAccountClient, fxAccount);
+ delegate.onComplete(null);
+ } else {
+ logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+ delegate.onComplete(null);
+ }
+ }
+
+ @Override
+ public void handleSuccess(FxAccountDevice result) {
+ Log.i(LOG_TAG, "Device registration complete");
+ Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
+ fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
+ delegate.onComplete(result.id);
+ }
+ });
}
@Override
- public void handleFailure(FxAccountClientRemoteException error) {
- Log.e(LOG_TAG, "Error while updating a device registration: ", error);
- if (error.httpStatusCode == 400) {
- if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
- recoverFromUnknownDevice(fxAccount);
- delegate.onComplete(null);
- } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
- recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount,
- context, delegate); // Will call delegate.onComplete
- }
- } else
- if (error.httpStatusCode == 401
- && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
- handleTokenError(error, fxAccountClient, fxAccount);
- delegate.onComplete(null);
- } else {
- logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
- delegate.onComplete(null);
- }
- }
-
- @Override
- public void handleSuccess(FxAccountDevice result) {
- Log.i(LOG_TAG, "Device registration complete");
- Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
- fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
- delegate.onComplete(result.id);
+ public void onError(Bundle message) {
+ Log.e(LOG_TAG, "Device Push endpoint registration failed");
+ delegate.onComplete(null);
}
});
}
private static void logErrorAndResetDeviceRegistrationVersion(
final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
Log.e(LOG_TAG, "Device registration failed", error);
fxAccount.resetDeviceRegistrationVersion();
@@ -232,9 +261,82 @@ public class FxAccountDeviceRegistrator
break;
}
}
}
onError();
}
});
}
+
+ // TODO: terrible inner class, refactor needed
+ // TODO: use reflection, wtf
+ public static class FxAMessaging implements BundleEventListener {
+ public interface EventResponseDelegate {
+ void onSuccess(Bundle message);
+ void onError(Bundle message);
+ }
+
+ private Map<String, EventResponseDelegate> callbacks = new HashMap<>();
+
+ private void sendRequest(String event, JSONObject message) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class<Enum> GeckoThreadStateClass = (Class<Enum>) Class.forName("org.mozilla.gecko.GeckoThread$State");
+ @SuppressWarnings("unchecked")
+ Object profileReadyState = Enum.valueOf(GeckoThreadStateClass, "PROFILE_READY");
+
+ Class<?> GeckoAppShellClass = Class.forName("org.mozilla.gecko.GeckoAppShell");
+ Method notifyObservers = GeckoAppShellClass.getMethod("notifyObservers", String.class, String.class, GeckoThreadStateClass);
+ notifyObservers.invoke(null, event, message.toString(), profileReadyState);
+ } catch(NoSuchMethodException | ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
+ Log.d(LOG_TAG, "Unable to sendRequest()", e);
+ }
+ }
+
+ private void sendRequestForResult(String event, JSONObject message, EventResponseDelegate delegate) {
+ String id = UUID.randomUUID().toString();
+ String expectedEvent = event + ":Response";
+
+ // TODO UGLY
+ try {
+ Class<?> EventDispatcherClass = Class.forName("org.mozilla.gecko.EventDispatcher");
+ Method getInstance = EventDispatcherClass.getMethod("getInstance");
+ Object eventDispatcher = getInstance.invoke(null);
+ Method registerBackgroundThreadListener = EventDispatcherClass.getMethod("registerBackgroundThreadListener", BundleEventListener.class, String[].class);
+ registerBackgroundThreadListener.invoke(eventDispatcher, this, new String[] {expectedEvent});
+ } catch(NoSuchMethodException | ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
+ Log.d(LOG_TAG, "Unable to get EventDispatcher", e);
+ delegate.onError(null);
+ return;
+ }
+
+ callbacks.put(id, delegate);
+
+ try {
+ message.put("__guid__", id);
+ } catch (JSONException e) {
+ delegate.onError(null);
+ return;
+ }
+ sendRequest(event, message);
+ }
+
+ @Override
+ public void handleMessage(String event, Bundle message, EventCallback callback) {
+ // TODO NEED UNREGISTER WITH REFLECTION
+ //EventDispatcher.getInstance().unregisterBackgroundThreadListener(this, event);
+
+ String id = message.getString("__guid__");
+ if (TextUtils.isEmpty(id) || !callbacks.containsKey(id)) {
+ Log.d(LOG_TAG, "Received an event response we have no callback for: " + event);
+ return;
+ }
+
+ EventResponseDelegate cb = callbacks.remove(id);
+ if (message.getString("status").equals("success")) {
+ cb.onSuccess(message.getBundle("response"));
+ } else {
+ cb.onError(message);
+ }
+ }
+ }
}