Bug 1080474 - Part 2 - expose device information via PresentationDeviceInfoManager API. r=khuey r=fabrice.
authorShih-Chiang Chien <schien@mozilla.com>
Fri, 14 Nov 2014 13:55:24 -0800
changeset 250686 83db4a079fd6840a160cb71619d99dfc305e58d2
parent 250685 8107ed414207bf34020a392cfe857b50fbaf8079
child 250687 650171a3db75a44918fa87c2cf97de60475ae947
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskhuey, fabrice
bugs1080474
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1080474 - Part 2 - expose device information via PresentationDeviceInfoManager API. r=khuey r=fabrice.
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/apps/PermissionsTable.jsm
dom/permission/tests/mochitest.ini
dom/permission/tests/test_presentation-device-manage.html
dom/presentation/PresentationDeviceInfoManager.js
dom/presentation/PresentationDeviceInfoManager.jsm
dom/presentation/PresentationDeviceInfoManager.manifest
dom/presentation/moz.build
dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
dom/presentation/tests/mochitest/mochitest.ini
dom/presentation/tests/mochitest/test_presentation_device_info.html
dom/presentation/tests/mochitest/test_presentation_device_info_permission.html
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/PresentationDeviceInfoManager.webidl
dom/webidl/moz.build
mobile/android/installer/package-manifest.in
modules/libpref/init/all.js
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -400,16 +400,18 @@
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/nsAsyncShutdown.manifest
 @BINPATH@/components/nsAsyncShutdown.js
 @BINPATH@/components/htmlMenuBuilder.js
 @BINPATH@/components/htmlMenuBuilder.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.js
 
 ; WiFi, NetworkManager, NetworkStats
 #ifdef MOZ_WIDGET_GONK
 @BINPATH@/components/DOMWifiManager.js
 @BINPATH@/components/DOMWifiManager.manifest
 @BINPATH@/components/DOMWifiP2pManager.js
 @BINPATH@/components/DOMWifiP2pManager.manifest
 @BINPATH@/components/NetworkInterfaceListService.js
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -604,16 +604,19 @@
 
 #ifdef MOZ_WEBSPEECH
 @RESPATH@/components/dom_webspeechsynth.xpt
 #endif
 
 @RESPATH@/components/nsAsyncShutdown.manifest
 @RESPATH@/components/nsAsyncShutdown.js
 
+@RESPATH@/components/PresentationDeviceInfoManager.manifest
+@RESPATH@/components/PresentationDeviceInfoManager.js
+
 ; InputMethod API
 @RESPATH@/components/MozKeyboard.js
 @RESPATH@/components/InputMethod.manifest
 
 #ifdef MOZ_DEBUG
 @RESPATH@/components/TestInterfaceJS.js
 @RESPATH@/components/TestInterfaceJS.manifest
 #endif
--- a/dom/apps/PermissionsTable.jsm
+++ b/dom/apps/PermissionsTable.jsm
@@ -521,16 +521,22 @@ this.PermissionsTable =  { geolocation: 
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "before-after-keyboard-event": {
                              app: DENY_ACTION,
                              trusted: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
+                           },
+                           "presentation-device-manage": {
+                             app: DENY_ACTION,
+                             trusted: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
                            }
                          };
 
 /**
  * Append access modes to the permission name as suffixes.
  *   e.g. permission name 'contacts' with ['read', 'write'] =
  *   ['contacts-read', contacts-write']
  * @param string aPermName
--- a/dom/permission/tests/mochitest.ini
+++ b/dom/permission/tests/mochitest.ini
@@ -9,16 +9,17 @@ support-files =
 [test_embed-apps.html]
 skip-if = ((buildapp == 'mulet' || buildapp == 'b2g') && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_idle.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure
 [test_permission_basics.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' # b2g(https not working, bug 907770) b2g-debug(https not working, bug 907770) b2g-desktop(Bug 907770)
 [test_permissions.html]
 [test_power.html]
+[test_presentation-device-manage.html]
 [test_systemXHR.html]
 [test_tcp-socket.html]
 [test_udp-socket.html]
 [test_webapps-manage.html]
 [test_camera.html]
 disabled = disabled until bug 859593 is fixed
 [test_keyboard.html]
 skip-if = toolkit == 'android'
new file mode 100644
--- /dev/null
+++ b/dom/permission/tests/test_presentation-device-manage.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1080474
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for presentation-device-manage permission</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1080474">test presentation-device-manage</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript;version=1.8" src="file_framework.js"></script>
+<script type="application/javascript;version=1.8">
+function verifier(success, failure) {
+  if (window.navigator.mozPresentationDeviceInfo) {
+    success("Got mozPresentationDeviceInfo object!");
+  } else {
+    failure("Failed to get mozPresentationDeviceInfo object!");
+  }
+}
+var gData = [
+  {
+    perm: ["presentation-device-manage"],
+    settings: [["dom.presentation.enabled", true]],
+    obj: "mozPresentationDeviceInfo",
+    webidl: "PresentationDeviceInfoManager"
+  }
+]
+</script>
+</pre>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.js
@@ -0,0 +1,120 @@
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+function log(aMsg) {
+  //dump("-*- PresentationDeviceInfoManager.js : " + aMsg + "\n");
+}
+
+const PRESENTATIONDEVICEINFOMANAGER_CID = Components.ID("{1bd66bef-f643-4be3-b690-0c656353eafd}");
+const PRESENTATIONDEVICEINFOMANAGER_CONTRACTID = "@mozilla.org/presentation-device/deviceInfo;1";
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+function PresentationDeviceInfoManager() {}
+
+PresentationDeviceInfoManager.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  classID: PRESENTATIONDEVICEINFOMANAGER_CID,
+  contractID: PRESENTATIONDEVICEINFOMANAGER_CONTRACTID,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+
+  receiveMessage: function(aMsg) {
+    if (!aMsg || !aMsg.data) {
+      return;
+    }
+
+    let data = aMsg.data;
+
+    log("receive aMsg: " + aMsg.name);
+    switch (aMsg.name) {
+      case "PresentationDeviceInfoManager:OnDeviceChange": {
+        let detail = {
+          detail: {
+            type: data.type,
+            deviceInfo: data.deviceInfo,
+          }
+        };
+        let event = new this._window.CustomEvent("devicechange", Cu.cloneInto(detail, this._window));
+        this.__DOM_IMPL__.dispatchEvent(event);
+        break;
+      }
+      case "PresentationDeviceInfoManager:GetAll:Result:Ok": {
+        let resolver = this.takePromiseResolver(data.requestId);
+
+        if (!resolver) {
+          return;
+        }
+
+        resolver.resolve(Cu.cloneInto(data.devices, this._window));
+        break;
+      }
+      case "PresentationDeviceInfoManager:GetAll:Result:Error": {
+        let resolver = this.takePromiseResolver(data.requestId);
+
+        if (!resolver) {
+          return;
+        }
+
+        resolver.reject(data.error);
+        break;
+      }
+    }
+  },
+
+  init: function(aWin) {
+    log("init");
+    this.initDOMRequestHelper(aWin, [
+      {name: "PresentationDeviceInfoManager:OnDeviceChange", weakRef: true},
+      {name: "PresentationDeviceInfoManager:GetAll:Result:Ok", weakRef: true},
+      {name: "PresentationDeviceInfoManager:GetAll:Result:Error", weakRef: true},
+    ]);
+  },
+
+  uninit: function() {
+    log("uninit");
+    let self = this;
+
+    this.forEachPromiseResolver(function(aKey) {
+      self.takePromiseResolver(aKey).reject("PresentationDeviceInfoManager got destroyed");
+    });
+  },
+
+  get ondevicechange() {
+    return this.__DOM_IMPL__.getEventHandler("ondevicechange");
+  },
+
+  set ondevicechange(aHandler) {
+    this.__DOM_IMPL__.setEventHandler("ondevicechange", aHandler);
+  },
+
+  getAll: function() {
+    log("getAll");
+    let self = this;
+    return this.createPromise(function(aResolve, aReject) {
+      let resolverId = self.getPromiseResolverId({ resolve: aResolve, reject: aReject });
+      cpmm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll", {
+        requestId: resolverId,
+      });
+    });
+  },
+
+  forceDiscovery: function() {
+    cpmm.sendAsyncMessage("PresentationDeviceInfoManager:ForceDiscovery");
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationDeviceInfoManager]);
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.jsm
@@ -0,0 +1,104 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = ["PresentationDeviceInfoService"];
+
+function log(aMsg) {
+  //dump("PresentationDeviceInfoManager.jsm: " + aMsg + "\n");
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "presentationDeviceManager",
+                                   "@mozilla.org/presentation-device/manager;1",
+                                   "nsIPresentationDeviceManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageBroadcaster");
+
+this.PresentationDeviceInfoService = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
+                                         Ci.nsIObserver]),
+
+  init: function() {
+    log("init");
+    ppmm.addMessageListener("PresentationDeviceInfoManager:GetAll", this);
+    ppmm.addMessageListener("PresentationDeviceInfoManager:ForceDiscovery", this);
+    Services.obs.addObserver(this, "presentation-device-change", false);
+  },
+
+  getAll: function(aData, aMm) {
+    log("getAll");
+    let deviceArray = presentationDeviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray);
+    if (!deviceArray) {
+      aData.error = "DataError";
+      aMm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll:Result:Error", aData);
+      return;
+    }
+
+    aData.devices = [];
+    for (let i = 0; i < deviceArray.length; i++) {
+      let device = deviceArray.queryElementAt(i, Ci.nsIPresentationDevice);
+      aData.devices.push({
+        id: device.id,
+        name: device.name,
+        type: device.type,
+      });
+    }
+    aMm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll:Result:Ok", aData);
+  },
+
+  forceDiscovery: function() {
+    log("forceDiscovery");
+    presentationDeviceManager.forceDiscovery();
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    log("observe: " + aTopic);
+
+    let device = aSubject.QueryInterface(Ci.nsIPresentationDevice);
+    let data = {
+      type: aData,
+      deviceInfo: {
+        id: device.id,
+        name: device.name,
+        type: device.type,
+      },
+    };
+    ppmm.broadcastAsyncMessage("PresentationDeviceInfoManager:OnDeviceChange", data);
+  },
+
+  receiveMessage: function(aMessage) {
+    if (!aMessage.target.assertPermission("presentation-device-manage")) {
+      debug("receive message " + aMessage.name +
+            " from a content process with no 'presentation-device-manage' privileges.");
+      return null;
+    }
+
+    let msg = aMessage.data || {};
+    let mm = aMessage.target;
+
+    log("receiveMessage: " + aMessage.name);
+    switch (aMessage.name) {
+      case "PresentationDeviceInfoManager:GetAll": {
+        this.getAll(msg, mm);
+        break;
+      }
+      case "PresentationDeviceInfoManager:ForceDiscovery": {
+        this.forceDiscovery();
+        break;
+      }
+    }
+  },
+};
+
+this.PresentationDeviceInfoService.init();
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.manifest
@@ -0,0 +1,3 @@
+# PresentationDeviceInfoManager.js
+component {1bd66bef-f643-4be3-b690-0c656353eafd} PresentationDeviceInfoManager.js
+contract @mozilla.org/presentation-device/deviceInfo;1 {1bd66bef-f643-4be3-b690-0c656353eafd}
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -2,23 +2,33 @@
 # 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/.
 
 DIRS += ['interfaces']
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
 
 EXPORTS.mozilla.dom.presentation += [
     'PresentationDeviceManager.h',
 ]
 
 SOURCES += [
     'PresentationDeviceManager.cpp',
     'PresentationSessionRequest.cpp',
 ]
 
+EXTRA_COMPONENTS += [
+    'PresentationDeviceInfoManager.js',
+    'PresentationDeviceInfoManager.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'PresentationDeviceInfoManager.jsm',
+]
+
 FAIL_ON_WARNINGS = True
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/PresentationDeviceInfoManager.jsm');
+
+const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+const manager = Cc['@mozilla.org/presentation-device/manager;1']
+                  .getService(Ci.nsIPresentationDeviceManager);
+
+var testProvider = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
+  forceDiscovery: function() {
+    sendAsyncMessage('force-discovery');
+  },
+  listener: null,
+};
+
+var testDevice = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+  establishSessionTransport: function(url, presentationId) {
+    return null;
+  },
+  id: null,
+  name: null,
+  type: null,
+  listener: null,
+};
+
+addMessageListener('setup', function() {
+  manager.addDeviceProvider(testProvider);
+
+  sendAsyncMessage('setup-complete');
+});
+
+addMessageListener('trigger-device-add', function(device) {
+  testDevice.id = device.id;
+  testDevice.name = device.name;
+  testDevice.type = device.type;
+  manager.addDevice(testDevice);
+});
+
+addMessageListener('trigger-device-update', function(device) {
+  testDevice.id = device.id;
+  testDevice.name = device.name;
+  testDevice.type = device.type;
+  manager.updateDevice(testDevice);
+});
+
+addMessageListener('trigger-device-remove', function() {
+  manager.removeDevice(testDevice);
+});
+
+addMessageListener('teardown', function() {
+  manager.removeDeviceProvider(testProvider);
+});
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  PresentationDeviceInfoChromeScript.js
+
+[test_presentation_device_info.html]
+[test_presentation_device_info_permission.html]
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test for B2G Presentation Device Info API</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1080474">Test for B2G Presentation Device Info API</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+var testDevice = {
+  id: 'id',
+  name: 'name',
+  type: 'type',
+};
+
+var gUrl = SimpleTest.getTestFileURL('PresentationDeviceInfoChromeScript.js');
+var gScript = SpecialPowers.loadChromeScript(gUrl);
+
+function testSetup() {
+  return new Promise(function(resolve, reject) {
+    gScript.addMessageListener('setup-complete', function() {
+      resolve();
+    });
+    gScript.sendAsyncMessage('setup');
+  });
+}
+
+function testForceDiscovery() {
+  info('test force discovery');
+  return new Promise(function(resolve, reject) {
+    gScript.addMessageListener('force-discovery', function() {
+      ok(true, 'nsIPresentationDeviceProvider.forceDiscovery is invoked');
+      resolve();
+    });
+    navigator.mozPresentationDeviceInfo.forceDiscovery();
+  });
+}
+
+function testDeviceAdd() {
+  info('test device add');
+  return new Promise(function(resolve, reject) {
+    navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+      navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+      let detail = e.detail;
+      is(detail.type, 'add', 'expected update type');
+      is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+      is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+      is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+      navigator.mozPresentationDeviceInfo.getAll()
+      .then(function(devices) {
+        is(devices.length, 1, 'expected 1 available device');
+        is(devices[0].id, testDevice.id, 'expected device id');
+        is(devices[0].name, testDevice.name, 'expected device name');
+        is(devices[0].type, testDevice.type, 'expected device type');
+        resolve();
+      });
+    });
+    gScript.sendAsyncMessage('trigger-device-add', testDevice);
+  });
+}
+
+function testDeviceUpdate() {
+  info('test device update');
+  return new Promise(function(resolve, reject) {
+    testDevice.name = 'name-update';
+
+    navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+      navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+      let detail = e.detail;
+      is(detail.type, 'update', 'expected update type');
+      is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+      is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+      is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+      navigator.mozPresentationDeviceInfo.getAll()
+      .then(function(devices) {
+        is(devices.length, 1, 'expected 1 available device');
+        is(devices[0].id, testDevice.id, 'expected device id');
+        is(devices[0].name, testDevice.name, 'expected device name');
+        is(devices[0].type, testDevice.type, 'expected device type');
+        resolve();
+      });
+    });
+    gScript.sendAsyncMessage('trigger-device-update', testDevice);
+  });
+}
+
+function testDeviceRemove() {
+  info('test device remove');
+  return new Promise(function(resolve, reject) {
+    navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+      navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+      let detail = e.detail;
+      is(detail.type, 'remove', 'expected update type');
+      is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+      is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+      is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+      navigator.mozPresentationDeviceInfo.getAll()
+      .then(function(devices) {
+        is(devices.length, 0, 'expected 0 available device');
+        resolve();
+      });
+    });
+    gScript.sendAsyncMessage('trigger-device-remove');
+  });
+}
+
+function runTests() {
+  testSetup()
+  .then(testForceDiscovery)
+  .then(testDeviceAdd)
+  .then(testDeviceUpdate)
+  .then(testDeviceRemove)
+  .then(function() {
+    info('test finished, teardown');
+    gScript.sendAsyncMessage('teardown', '');
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+}
+
+window.addEventListener('load', function() {
+  SpecialPowers.pushPermissions([
+    {type: 'presentation-device-manage', allow: true, context: document},
+  ], function() {
+    SpecialPowers.pushPrefEnv({
+      'set': [
+        ['dom.presentation.enabled', true],
+      ]
+    }, runTests);
+  });
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test for B2G Presentation Device Info API Permission</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1080474">Test for B2G Presentation Device Info API Permission</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+  ise(navigator.mozPresentationDeviceInfo, undefined, 'navigator.mozPresentationDeviceInfo is undefined');
+  SimpleTest.finish();
+}
+
+window.addEventListener('load', function() {
+  SpecialPowers.pushPermissions([
+    {type: 'presentation-device-manage', allow: false, context: document},
+  ], function() {
+    SpecialPowers.pushPrefEnv({
+      'set': [
+        ['dom.presentation.enabled', true],
+      ]
+    }, runTests);
+  });
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -879,16 +879,20 @@ var interfaceNamesInGlobalScope =
     "PopStateEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopupBlockedEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PopupBoxObject", xbl: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PositionSensorVRDevice", pref: "dom.vr.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "PresentationDeviceInfoManager",
+     pref: "dom.presentation.enabled",
+     permission: ["presentation-device-manage"]},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "ProcessingInstruction",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProgressEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Promise",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PropertyNodeList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/webidl/PresentationDeviceInfoManager.webidl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+dictionary PresentationDeviceInfo {
+  DOMString id;
+  DOMString name;
+  DOMString type;
+};
+
+[NavigatorProperty="mozPresentationDeviceInfo",
+ JSImplementation="@mozilla.org/presentation-device/deviceInfo;1",
+ Pref="dom.presentation.enabled",
+ CheckPermissions="presentation-device-manage"]
+interface PresentationDeviceInfoManager : EventTarget {
+  // notify if any device updated.
+  attribute EventHandler ondevicechange;
+
+  // retrieve all available device infos
+  Promise<sequence<PresentationDeviceInfo>> getAll();
+
+  // Force all registered device provider to update device information.
+  void forceDiscovery();
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -326,16 +326,17 @@ WEBIDL_FILES = [
     'PermissionSettings.webidl',
     'PhoneNumberService.webidl',
     'Plugin.webidl',
     'PluginArray.webidl',
     'PointerEvent.webidl',
     'PopupBoxObject.webidl',
     'Position.webidl',
     'PositionError.webidl',
+    'PresentationDeviceInfoManager.webidl',
     'ProcessingInstruction.webidl',
     'ProfileTimelineMarker.webidl',
     'Promise.webidl',
     'PromiseDebugging.webidl',
     'PushManager.webidl',
     'RadioNodeList.webidl',
     'Range.webidl',
     'Rect.webidl',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -451,16 +451,19 @@
 #endif
 
 @BINPATH@/components/nsAsyncShutdown.manifest
 @BINPATH@/components/nsAsyncShutdown.js
 
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 
+@BINPATH@/components/PresentationDeviceInfoManager.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.js
+
 ; Modules
 @BINPATH@/modules/*
 
 #ifdef MOZ_SAFE_BROWSING
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4417,16 +4417,19 @@ pref("camera.control.low_memory_threshol
 #endif
 
 // UDPSocket API
 pref("dom.udpsocket.enabled", false);
 
 // Disable before keyboard events and after keyboard events by default.
 pref("dom.beforeAfterKeyboardEvent.enabled", false);
 
+// Presentation API
+pref("dom.presentation.enabled", false);
+
 // Use raw ICU instead of CoreServices API in Unicode collation
 #ifdef XP_MACOSX
 pref("intl.collation.mac.use_icu", true);
 #endif
 
 // Enable meta-viewport support in remote APZ-enabled frames.
 pref("dom.meta-viewport.enabled", false);