Bug 1322586 - 3. Add GeckoViewPermission.js for handling permissions; r=esawin draft
authorJim Chen <nchen@mozilla.com>
Thu, 20 Jul 2017 17:52:14 -0400
changeset 612608 fe949fde01ddba207b12f2bd49e52ea2bc8b8c5e
parent 612607 3ab93052de3676244843c55c50ae5ade63ab8f11
child 612609 7c0a4489b136100ca57740512436bb8cccc62f3a
push id69550
push userbmo:nchen@mozilla.com
push dateThu, 20 Jul 2017 21:53:06 +0000
reviewersesawin
bugs1322586
milestone56.0a1
Bug 1322586 - 3. Add GeckoViewPermission.js for handling permissions; r=esawin Add GeckoViewPermission.js, which is responsible for handling requests from Gecko for permissions, and passing those requests to GeckoView. MozReview-Commit-ID: C0g24hKEYbf
mobile/android/components/geckoview/GeckoView.manifest
mobile/android/components/geckoview/GeckoViewPermission.js
mobile/android/components/geckoview/moz.build
mobile/android/installer/package-manifest.in
--- a/mobile/android/components/geckoview/GeckoView.manifest
+++ b/mobile/android/components/geckoview/GeckoView.manifest
@@ -1,11 +1,16 @@
 # Stylesheets
 category agent-style-sheets browser-content-stylesheet chrome://geckoview/skin/content.css
 
+# GeckoViewPermission.js
+component {42f3c238-e8e8-4015-9ca2-148723a8afcf} GeckoViewPermission.js
+contract @mozilla.org/content-permission/prompt;1 {42f3c238-e8e8-4015-9ca2-148723a8afcf}
+category app-startup GeckoViewPermission service,@mozilla.org/content-permission/prompt;1
+
 # GeckoViewPrompt.js
 component {076ac188-23c1-4390-aa08-7ef1f78ca5d9} GeckoViewPrompt.js
 contract @mozilla.org/embedcomp/prompt-service;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9}
 contract @mozilla.org/prompter;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9}
 category app-startup GeckoViewPrompt service,@mozilla.org/prompter;1
 category profile-after-change GeckoViewPrompt @mozilla.org/prompter;1 process=main
 component {aa0dd6fc-73dd-4621-8385-c0b377e02cee} GeckoViewPrompt.js process=main
 contract @mozilla.org/colorpicker;1 {aa0dd6fc-73dd-4621-8385-c0b377e02cee} process=main
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewPermission.js
@@ -0,0 +1,250 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
+                                  "resource://gre/modules/Messaging.jsm");
+
+// See: http://developer.android.com/reference/android/Manifest.permission.html
+const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
+const PERM_CAMERA = "android.permission.CAMERA";
+const PERM_RECORD_AUDIO = "android.permission.RECORD_AUDIO";
+
+function GeckoViewPermission() {
+}
+
+GeckoViewPermission.prototype = {
+  classID: Components.ID("{42f3c238-e8e8-4015-9ca2-148723a8afcf}"),
+
+  QueryInterface: XPCOMUtils.generateQI([
+      Ci.nsIObserver, Ci.nsIContentPermissionPrompt]),
+
+  _appPermissions: {},
+
+  /* ----------  nsIObserver  ---------- */
+  observe: function(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "app-startup": {
+        Services.obs.addObserver(this, "getUserMedia:ask-device-permission");
+        Services.obs.addObserver(this, "getUserMedia:request");
+        Services.obs.addObserver(this, "PeerConnection:request");
+        break;
+      }
+      case "getUserMedia:ask-device-permission": {
+        this.handleMediaAskDevicePermission(aData, aSubject);
+        break;
+      }
+      case "getUserMedia:request": {
+        this.handleMediaRequest(aSubject);
+        break;
+      }
+      case "PeerConnection:request": {
+        this.handlePeerConnectionRequest(aSubject);
+        break;
+      }
+    }
+  },
+
+  handleMediaAskDevicePermission: function(aType, aCallback) {
+    let perms = [];
+    if (aType === "video" || aType === "all") {
+      perms.push(PERM_CAMERA);
+    }
+    if (aType === "audio" || aType === "all") {
+      perms.push(PERM_RECORD_AUDIO);
+    }
+
+    let dispatcher = this.getActiveDispatcher();
+    let callback = _ => {
+      Services.obs.notifyObservers(aCallback, "getUserMedia:got-device-permission");
+    };
+
+    if (dispatcher) {
+      this.getAppPermissions(dispatcher, perms).then(callback, callback);
+    } else {
+      // No dispatcher; just bail.
+      callback();
+    }
+  },
+
+  handleMediaRequest: function(aRequest) {
+    let constraints = aRequest.getConstraints();
+    let callId = aRequest.callID;
+    let denyRequest = _ => {
+      Services.obs.notifyObservers(null, "getUserMedia:response:deny", callId);
+    };
+
+    let win = Services.wm.getOuterWindowWithId(aRequest.windowID);
+    new Promise((resolve, reject) => {
+      win.navigator.mozGetUserMediaDevices(constraints, resolve, reject,
+                                           aRequest.innerWindowID, callId);
+      // Release the request first.
+      aRequest = undefined;
+    }).then(devices => {
+      if (win.closed) {
+        return;
+      }
+
+      let sources = devices.map(device => {
+        device = device.QueryInterface(Ci.nsIMediaDevice);
+        return {
+          type: device.type,
+          id: device.id,
+          rawId: device.rawId,
+          name: device.name,
+          mediaSource: device.mediaSource,
+        };
+      });
+
+      if (constraints.video && !sources.some(source => source.type === "video")) {
+        throw "no video source";
+      } else if (constraints.audio && !sources.some(source => source.type === "audio")) {
+        throw "no audio source";
+      }
+
+      let dispatcher = this.getDispatcherForWindow(win);
+      let uri = win.document.documentURIObject;
+      return dispatcher.sendRequestForResult({
+        type: "GeckoView:MediaPermission",
+        uri: uri.spec,
+        video: constraints.video ? sources.filter(source => source.type === "video") : null,
+        audio: constraints.audio ? sources.filter(source => source.type === "audio") : null,
+      }).then(response => {
+        if (!response) {
+          // Rejected.
+          denyRequest();
+          return;
+        }
+        let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+        if (constraints.video) {
+          let video = devices.find(device => response.video === device.id);
+          if (!video) {
+            throw new Error("invalid video id");
+          }
+          // Although the lifetime is "session" it will be removed upon
+          // use so it's more of a one-shot.
+          Services.perms.add(uri, "MediaManagerVideo",
+                             Services.perms.ALLOW_ACTION,
+                             Services.perms.EXPIRE_SESSION);
+          allowedDevices.appendElement(video);
+        }
+        if (constraints.audio) {
+          let audio = devices.find(device => response.audio === device.id);
+          if (!audio) {
+            throw new Error("invalid audio id");
+          }
+          allowedDevices.appendElement(audio);
+        }
+        Services.obs.notifyObservers(
+            allowedDevices, "getUserMedia:response:allow", callId);
+      });
+    }).catch(error => {
+      Cu.reportError("Media device error: " + error);
+      denyRequest();
+    });
+  },
+
+  handlePeerConnectionRequest: function(aRequest) {
+    Services.obs.notifyObservers(null, "PeerConnection:response:allow", aRequest.callID);
+  },
+
+  getActiveDispatcher: function() {
+    let getDispatcher = win => {
+      try {
+        let dispatcher = win.WindowEventDispatcher || EventDispatcher.for(win);
+        if (!win.closed && dispatcher) {
+          return dispatcher;
+        }
+      } catch (e) {
+        // Ignore.
+      }
+      return null;
+    };
+
+    let dispatcher = getDispatcher(Services.focus.activeWindow.top);
+    if (dispatcher) {
+      return dispatcher;
+    }
+
+    let iter = Services.wm.getEnumerator(/* windowType */ null);
+    while (iter.hasMoreElements()) {
+      dispatcher = getDispatcher(iter.getNext().QueryInterface(Ci.nsIDOMWindow).top);
+      if (dispatcher) {
+        return dispatcher;
+      }
+    }
+    return null;
+  },
+
+  getDispatcherForWindow: function(aWin) {
+    aWin = aWin.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDocShell).QueryInterface(Ci.nsIDocShellTreeItem)
+               .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindow);
+    return aWin.WindowEventDispatcher || EventDispatcher.for(aWin);
+  },
+
+  checkAppPermissions: function(aPerms) {
+    return aPerms.every(perm => this._appPermissions[perm]);
+  },
+
+  getAppPermissions: function(aDispatcher, aPerms) {
+    let perms = aPerms.filter(perm => !this._appPermissions[perm]);
+    if (!perms.length) {
+      return Promise.resolve(/* granted */ true);
+    }
+    return aDispatcher.sendRequestForResult({
+      type: "GeckoView:AndroidPermission",
+      perms: perms,
+    }).then(granted => {
+      if (granted) {
+        for (let perm of perms) {
+          this._appPermissions[perm] = true;
+        }
+      }
+      return granted;
+    });
+  },
+
+  prompt: function(aRequest) {
+    // Only allow exactly one permission request here.
+    let types = aRequest.types.QueryInterface(Ci.nsIArray);
+    if (types.length !== 1) {
+      aRequest.cancel();
+      return;
+    }
+
+    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+    let dispatcher = this.getDispatcherForWindow(
+        aRequest.window ? aRequest.window.top : aRequest.element.ownerGlobal.top);
+    let promise = dispatcher.sendRequestForResult({
+        type: "GeckoView:ContentPermission",
+        uri: aRequest.principal.URI.spec,
+        perm: perm.type,
+        access: perm.access !== "unused" ? perm.access : null,
+    }).then(granted => {
+      if (!granted) {
+        return false;
+      }
+      // Ask for app permission after asking for content permission.
+      if (perm.type === "geolocation") {
+        return this.getAppPermissions(dispatcher, [PERM_ACCESS_FINE_LOCATION]);
+      }
+      return true;
+    }).catch(error => {
+      Cu.reportError("Permission error: " + error);
+      return /* granted */ false;
+    }).then(granted => {
+      (granted ? aRequest.allow : aRequest.cancel)();
+      // Manually release the target request here to facilitate garbage collection.
+      aRequest = undefined;
+    });
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GeckoViewPermission]);
--- a/mobile/android/components/geckoview/moz.build
+++ b/mobile/android/components/geckoview/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_COMPONENTS += [
     'GeckoView.manifest',
+    'GeckoViewPermission.js',
     'GeckoViewPrompt.js',
 ]
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -524,16 +524,17 @@
 
 [mobile]
 @BINPATH@/chrome/geckoview@JAREXT@
 @BINPATH@/chrome/geckoview.manifest
 
 #ifdef MOZ_GECKOVIEW_JAR
 @BINPATH@/components/GeckoView.manifest
 @BINPATH@/components/GeckoViewPrompt.js
+@BINPATH@/components/GeckoViewPermission.js
 #else
 @BINPATH@/chrome/chrome@JAREXT@
 @BINPATH@/chrome/chrome.manifest
 @BINPATH@/components/AboutRedirector.js
 @BINPATH@/components/AddonUpdateService.js
 @BINPATH@/components/BlocklistPrompt.js
 @BINPATH@/components/BrowserCLH.js
 @BINPATH@/components/ColorPicker.js