Bug 1287643 - POC registration. draft
authorEdouard Oger <eoger@fastmail.com>
Wed, 20 Jul 2016 10:47:04 -0700
changeset 390883 12b192497d201ac8beabd51ad52577d20bf8dba7
parent 390881 c78fe9b004404df926b0b8117b789348e7d05ac2
child 526082 e2229a0702646223cbe17507dac96e9fb172903a
push id23761
push userbmo:edouard.oger@gmail.com
push dateThu, 21 Jul 2016 20:30:18 +0000
bugs1287643
milestone50.0a1
Bug 1287643 - POC registration. MozReview-Commit-ID: BYFMeQNgumu
mobile/android/chrome/content/FxAccountPush.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
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);
+      }
+    }
+  }
 }