Bug 1289974 part 1: Device selection for presentation API on Desktop; r=mconley draft
authorChun-Min Chang <chun.m.chang@gmail.com>
Tue, 15 Nov 2016 11:07:09 +0800
changeset 438823 aace9cf776c9da13f3b9daf8ff913c363f0fa7a4
parent 438818 5e76768327660437bf3486554ad318e4b70276e1
child 438824 2004c4392884585bf24e058f9ca244c02953d352
push id35824
push userbmo:cchang@mozilla.com
push dateTue, 15 Nov 2016 03:26:53 +0000
reviewersmconley
bugs1289974
milestone53.0a1
Bug 1289974 part 1: Device selection for presentation API on Desktop; r=mconley MozReview-Commit-ID: KKT8xsafuAQ
browser/extensions/moz.build
browser/extensions/presentation/bootstrap.js
browser/extensions/presentation/content/PresentationDevicePrompt.jsm
browser/extensions/presentation/install.rdf.in
browser/extensions/presentation/jar.mn
browser/extensions/presentation/locale/en-US/presentation.properties
browser/extensions/presentation/locale/jar.mn
browser/extensions/presentation/locale/moz.build
browser/extensions/presentation/moz.build
browser/extensions/presentation/skin/shared/link.svg
browser/locales/Makefile.in
testing/talos/talos/xtalos/xperf_whitelist.json
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -12,9 +12,10 @@ DIRS += [
     'webcompat',
 ]
 
 # Only include the following system add-ons if building Aurora or Nightly
 if 'a' in CONFIG['GRE_MILESTONE']:
     DIRS += [
         'flyweb',
         'formautofill',
+        'presentation',
     ]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/bootstrap.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PRESENTATION_DEVICE_PROMPT_PATH =
+  "chrome://presentation/content/PresentationDevicePrompt.jsm";
+
+function log(aMsg) {
+  // dump("@ Presentation: " + aMsg + "\n");
+}
+
+function install(aData, aReason) {
+}
+
+function uninstall(aData, aReason) {
+}
+
+function startup(aData, aReason) {
+  log("startup");
+  Presentation.init();
+}
+
+function shutdown(aData, aReason) {
+  log("shutdown");
+  Presentation.uninit();
+}
+
+// Register/unregister a constructor as a factory.
+function Factory() {}
+Factory.prototype = {
+  register: function(targetConstructor) {
+    let proto = targetConstructor.prototype;
+    this._classID = proto.classID;
+
+    let factory = XPCOMUtils._getFactory(targetConstructor);
+    this._factory = factory;
+
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    registrar.registerFactory(proto.classID, proto.classDescription,
+                              proto.contractID, factory);
+  },
+
+  unregister: function() {
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    registrar.unregisterFactory(this._classID, this._factory);
+    this._factory = null;
+    this._classID = null;
+  },
+};
+
+var Presentation = {
+  // PUBLIC APIs
+  init: function() {
+    log("init");
+    // Register PresentationDevicePrompt into a XPCOM component.
+    Cu.import(PRESENTATION_DEVICE_PROMPT_PATH);
+    this._register();
+  },
+
+  uninit: function() {
+    log("uninit");
+    // Unregister PresentationDevicePrompt XPCOM component.
+    this._unregister();
+    Cu.unload(PRESENTATION_DEVICE_PROMPT_PATH);
+  },
+
+  // PRIVATE APIs
+  _register: function() {
+    log("_register");
+    this._devicePromptFactory = new Factory();
+    this._devicePromptFactory.register(PresentationDevicePrompt);
+  },
+
+  _unregister: function() {
+    log("_unregister");
+    this._devicePromptFactory.unregister();
+    delete this._devicePromptFactory;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/PresentationDevicePrompt.jsm
@@ -0,0 +1,254 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/*
+ * This is the implementation of nsIPresentationDevicePrompt XPCOM.
+ * It will be registered into a XPCOM component by Presentation.jsm.
+ *
+ * This component will prompt a device selection UI for users to choose which
+ * devices they want to connect, when PresentationRequest is started.
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["PresentationDevicePrompt"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// An string bundle for localization.
+XPCOMUtils.defineLazyGetter(this, "Strings", function() {
+  return Services.strings.createBundle("chrome://presentation/locale/presentation.properties");
+});
+// To generate a device selection prompt.
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionUI",
+                                        "resource:///modules/PermissionUI.jsm");
+/*
+ * Utils
+ */
+function log(aMsg) {
+  // Prefix is useful to grep log.
+  // dump("@ PresentationDevicePrompt: " + aMsg + "\n");
+}
+
+function GetString(aName) {
+  return Strings.GetStringFromName(aName);
+}
+
+/*
+ * Device Selection UI
+ */
+const kNotificationId = "presentation-device-selection";
+const kNotificationPopupIcon = "chrome://presentation-shared/skin/link.svg";
+
+// There is no dependancy between kNotificationId and kNotificationAnchorId,
+// so it's NOT necessary to name them by same prefix
+// (e.g., presentation-device-selection-notification-icon).
+const kNotificationAnchorId = "presentation-device-notification-icon";
+const kNotificationAnchorIcon = "chrome://presentation-shared/skin/link.svg";
+
+// This will insert our own popupnotification content with the device list
+// into the displayed popupnotification element.
+// PopupNotifications.jsm will automatically generate a popupnotification
+// element whose id is <notification id> + "-notification" and show it,
+// so kPopupNotificationId must be kNotificationId + "-notification".
+// Read more detail in PopupNotifications._refreshPanel.
+const kPopupNotificationId = kNotificationId + "-notification";
+
+function PresentationPermissionPrompt(aRequest, aDevices) {
+  this.request = aRequest;
+  this._isResponded = false;
+  this._devices = aDevices;
+}
+
+PresentationPermissionPrompt.prototype = {
+  __proto__: PermissionUI.PermissionPromptForRequestPrototype,
+  // PUBLIC APIs
+  get browser() {
+    return this.request.chromeEventHandler;
+  },
+  get principal() {
+    return this.request.principal;
+  },
+  get popupOptions() {
+    return {
+      hideNotNow: true,
+      removeOnDismissal: true,
+      popupIconURL: kNotificationPopupIcon, // Icon shown on prompt content
+      eventCallback: (aTopic, aNewBrowser) => {
+        log("eventCallback: " + aTopic);
+        let handler = {
+          // dismissed: () => { // Won't be fired if removeOnDismissal is true.
+          //   log("Dismissed by user. Cancel the request.");
+          // },
+          removed: () => {
+            log("Prompt is removed.");
+            if (!this._isResponded) {
+              log("Dismissed by user. Cancel the request.");
+              this.request.cancel(Cr.NS_ERROR_NOT_AVAILABLE);
+            }
+          },
+          showing: () => {
+            log("Prompt is showing.");
+            // We cannot insert the device list at "showing" phase because
+            // the popupnotification content whose id is kPopupNotificationId
+            // is not generated yet.
+          },
+          shown: () => {
+            log("Prompt is shown.");
+            // Insert device selection list into popupnotification element.
+            this._createPopupContent();
+          },
+        };
+
+        // Call the handler for Notification events.
+        handler[aTopic]();
+      },
+    };
+  },
+  get notificationID() {
+    return kNotificationId;
+  },
+  get anchorID() {
+    let chromeDoc = this.browser.ownerDocument;
+    let anchor = chromeDoc.getElementById(kNotificationAnchorId);
+    if (!anchor) {
+      let notificationPopupBox =
+        chromeDoc.getElementById("notification-popup-box");
+      // Icon shown on URL bar
+      let notificationIcon = chromeDoc.createElement("image");
+      notificationIcon.id = kNotificationAnchorId;
+      notificationIcon.setAttribute("src", kNotificationAnchorIcon);
+      notificationIcon.classList.add("notification-anchor-icon");
+      notificationIcon.setAttribute("role", "button");
+      notificationIcon.setAttribute("tooltiptext",
+                                    GetString("presentation.urlbar.tooltiptext"));
+      notificationIcon.style.filter = "url('chrome://browser/skin/filters.svg#fill')";
+      notificationIcon.style.fill = "currentcolor";
+      notificationIcon.style.opacity = "0.4";
+      notificationPopupBox.appendChild(notificationIcon);
+    }
+
+    return kNotificationAnchorId;
+  },
+  get message() {
+    return GetString("presentation.message", this._domainName);
+  },
+  get promptActions() {
+    return [{
+      label: GetString("presentation.deviceprompt.select.label"),
+      accessKey: GetString("presentation.deviceprompt.select.accessKey"),
+      callback: () => {
+        log("Select");
+        this._isResponded = true;
+        if (!this._listbox || !this._devices.length) {
+          log("No device can be selected!");
+          this.request.cancel(Cr.NS_ERROR_NOT_AVAILABLE);
+          return;
+        }
+        let device = this._devices[this._listbox.selectedIndex];
+        this.request.select(device);
+        log("device: " + device.name + "(" + device.id + ") is selected!");
+      },
+    }, {
+      label: GetString("presentation.deviceprompt.cancel.label"),
+      accessKey: GetString("presentation.deviceprompt.cancel.accessKey"),
+      callback: () => {
+        log("Cancel selection.");
+        this._isResponded = true;
+        this.request.cancel(Cr.NS_ERROR_NOT_AVAILABLE);
+      },
+      dismiss: true, // For hideNotNow.
+    }];
+  },
+  // PRIVATE APIs
+  get _domainName() {
+    if (this.principal.URI instanceof Ci.nsIFileURL) {
+      return this.principal.URI.path.split('/')[1];
+    }
+    return this.principal.URI.hostPort;
+  },
+  _createPopupContent: function() {
+    log("_createPopupContent");
+
+    if (!this._devices.length) {
+      log("No available devices can be listed!");
+      return;
+    }
+
+    let chromeDoc = this.browser.ownerDocument;
+
+    let popupnotification = chromeDoc.getElementById(kPopupNotificationId);
+    if (!popupnotification) {
+      log("No available popupnotification element to be inserted!");
+      return;
+    }
+
+    let popupnotificationcontent =
+      chromeDoc.createElement("popupnotificationcontent");
+
+    this._listbox = chromeDoc.createElement("richlistbox");
+    this._listbox.setAttribute("flex", "1");
+    this._devices.forEach((device) => {
+      let listitem = chromeDoc.createElement("richlistitem");
+      let label = chromeDoc.createElement("label");
+      label.setAttribute("value", device.name);
+      listitem.appendChild(label);
+      this._listbox.appendChild(listitem);
+    });
+
+    popupnotificationcontent.appendChild(this._listbox);
+    popupnotification.appendChild(popupnotificationcontent);
+  },
+};
+
+
+/*
+ * nsIPresentationDevicePrompt
+ */
+// For XPCOM registration
+const PRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1";
+const PRESENTATIONDEVICEPROMPT_CID        = Components.ID("{388bd149-c919-4a43-b646-d7ec57877689}");
+
+function PresentationDevicePrompt() {}
+
+PresentationDevicePrompt.prototype = {
+  // properties required for XPCOM registration:
+  classID: PRESENTATIONDEVICEPROMPT_CID,
+  classDescription: "Presentation API Device Prompt",
+  contractID: PRESENTATIONDEVICEPROMPT_CONTRACTID,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]),
+
+  // This will be fired when window.PresentationRequest(URL).start() is called.
+  promptDeviceSelection: function(aRequest) {
+    log("promptDeviceSelection");
+
+    // Cancel request if no available device.
+    let devices = this._loadDevices();
+    if (!devices.length) {
+      log("No available device.");
+      aRequest.cancel(Cr.NS_ERROR_NOT_AVAILABLE);
+      return;
+    }
+
+    // Show the prompt to users.
+    let promptUI = new PresentationPermissionPrompt(aRequest, devices);
+    promptUI.prompt();
+  },
+  _loadDevices: function() {
+    let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                        .getService(Ci.nsIPresentationDeviceManager);
+    let devices = deviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray);
+    let list = [];
+    for (let i = 0; i < devices.length; i++) {
+      let device = devices.queryElementAt(i, Ci.nsIPresentationDevice);
+      list.push(device);
+    }
+
+    return list;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/install.rdf.in
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>presentation@mozilla.org</em:id>
+    <em:version>1.0.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <!-- Target Application this theme can install into,
+        with minimum and maximum supported versions. -->
+    <em:targetApplication>
+      <Description>
+        <!-- Firefox GUID -->
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Presentation</em:name>
+    <em:description>Discover nearby devices in the browser</em:description>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/jar.mn
@@ -0,0 +1,5 @@
+[features/presentation@mozilla.org] chrome.jar:
+% content presentation %content/
+  content/  (content/*)
+% skin presentation-shared classic/1.0 %skin/shared/
+  skin/  (skin/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/locale/en-US/presentation.properties
@@ -0,0 +1,6 @@
+presentation.message=Select one device to send the content.
+presentation.urlbar.tooltiptext=View the device-selection request
+presentation.deviceprompt.select.label=Send
+presentation.deviceprompt.select.accessKey=S
+presentation.deviceprompt.cancel.label=Cancel
+presentation.deviceprompt.cancel.accessKey=C
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/locale/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# 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/.
+
+[features/presentation@mozilla.org] @AB_CD@.jar:
+% locale presentation @AB_CD@ %locale/@AB_CD@/
+  locale/@AB_CD@/                    (en-US/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/locale/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/moz.build
@@ -0,0 +1,14 @@
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+DIRS += ['locale']
+
+FINAL_TARGET_FILES.features['presentation@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['presentation@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/skin/shared/link.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="gray" x="0px" y="0px" width="32px"
+	 height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="layer_2">
+</g>
+<g id="layer_1">
+	<g>
+		<g>
+			<path fill-rule="evenodd" clip-rule="evenodd" d="M16.968,9.895c3.293,0,6.117,1.995,7.338,4.841l1.603-1.216
+				c-1.628-3.3-4.994-5.591-8.923-5.591c-3.942,0-7.319,2.305-8.94,5.624l1.594,1.166C10.865,11.883,13.682,9.895,16.968,9.895z
+				 M17.135,13.964c1.948,0,3.566,1.397,3.917,3.244l1.779-1.35c-0.849-2.276-3.023-3.904-5.595-3.904
+				c-2.669,0-4.904,1.758-5.677,4.171l1.647,1.205C13.509,15.424,15.145,13.964,17.135,13.964z M8.756,16.025H1.833
+				c-0.737,0-1.729,0.598-1.729,1.335v11.271c0,0.738,0.596,1.335,1.334,1.335h7.318c0.738,0,1.336-0.597,1.336-1.335V17.36
+				C10.092,16.623,9.494,16.025,8.756,16.025z M8.113,27.472c0,0.299-0.243,0.541-0.541,0.541H2.599
+				c-0.298,0-0.541-0.242-0.541-0.541v-8.949c0-0.299,0.243-0.541,0.541-0.541h4.973c0.298,0,0.541,0.242,0.541,0.541V27.472z
+				 M15.246,17.992c0,1.064,0.862,1.926,1.926,1.926c1.064,0,1.926-0.862,1.926-1.926c0-1.064-0.862-1.926-1.926-1.926
+				C16.108,16.066,15.246,16.929,15.246,17.992z M29.962,2.034H4.011c-1.102,0-1.996,0.894-1.996,1.996v10.008h4.042V5.937
+				c0-1.102,0.894-1.996,1.996-1.996h17.973c1.103,0,1.996,0.894,1.996,1.996v16.075c0,1.103-0.894,1.996-1.996,1.996H12.089v1.918
+				h17.873c1.102,0,1.996-0.894,1.996-1.996V4.03C31.958,2.927,31.064,2.034,29.962,2.034z"/>
+		</g>
+	</g>
+</g>
+</svg>
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -95,16 +95,17 @@ searchplugins:: $(list-json)
 DEFINES += -DBOOKMARKS_INCLUDE_DIR=$(dir $(call MERGE_FILE,profile/bookmarks.inc))
 
 libs-%:
 	$(NSINSTALL) -D $(DIST)/install
 	@$(MAKE) -C ../../toolkit/locales libs-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
 	@$(MAKE) -C ../../services/sync/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../extensions/spellcheck/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../extensions/pocket/locale AB_CD=$* XPI_NAME=locale-$*
+	@$(MAKE) -C ../extensions/presentation/locale AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../intl/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../devtools/client/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
 	@$(MAKE) -B searchplugins AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=$(PREF_DIR)
 	@$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales AB_CD=$* XPI_NAME=locale-$*
 
 repackage-win32-installer: WIN32_INSTALLER_OUT=$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe
 repackage-win32-installer: $(call ESCAPE_WILDCARD,$(WIN32_INSTALLER_IN)) $(SUBMAKEFILES) libs-$(AB_CD)
--- a/testing/talos/talos/xtalos/xperf_whitelist.json
+++ b/testing/talos/talos/xtalos/xperf_whitelist.json
@@ -1,41 +1,42 @@
-{"{firefox}\\Crash Reports\\{time}": {"ignore": true}, 
- "C:\\$Mft": {"ignore": true}, 
- "C:\\$Extend\\$UsnJrnl:$J": {"ignore": true}, 
- "C:\\Windows\\Prefetch\\{prefetch}.pf": {"ignore": true}, 
+{"{firefox}\\Crash Reports\\{time}": {"ignore": true},
+ "C:\\$Mft": {"ignore": true},
+ "C:\\$Extend\\$UsnJrnl:$J": {"ignore": true},
+ "C:\\Windows\\Prefetch\\{prefetch}.pf": {"ignore": true},
  "C:\\$Secure": {"ignore": true},
  "C:\\$logfile": {"ignore": true},
  "{firefox}\\omni.ja": {"mincount": 0, "maxcount": 46, "minbytes": 0, "maxbytes": 3014656},
  "{firefox}\\browser\\omni.ja": {"mincount": 0, "maxcount": 28, "minbytes": 0, "maxbytes": 1835008},
  "{firefox}\\browser\\features\\aushelper@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\e10srollout@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\flyweb@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\formautofill@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\loop@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\firefox@getpocket.com.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
+ "{firefox}\\browser\\features\\presentation@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{firefox}\\browser\\features\\webcompat@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
  "{talos}\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
  "{talos}\\talos\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
  "{talos}\\tests\\tp5n\\tp5n.manifest.develop": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
  "{talos}\\talos\\tests\\tp5n\\tp5n.manifest.develop": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
  "{profile}\\localstore.rdf": {"mincount": 2, "maxcount": 2, "minbytes": 8192, "maxbytes": 8192},
  "{firefox}\\dependentlibs.list": {"mincount": 4, "maxcount": 4, "minbytes": 16384, "maxbytes": 16384},
- "{profile}\\content-prefs.sqlite": {"mincount": 6, "maxcount": 6, "minbytes": 65768, "maxbytes": 65768}, 
+ "{profile}\\content-prefs.sqlite": {"mincount": 6, "maxcount": 6, "minbytes": 65768, "maxbytes": 65768},
  "{profile}\\extensions.ini": {"mincount": 2, "maxcount": 2, "minbytes": 8192, "maxbytes": 8192},
  "{profile}\\extensions\\pageloader@mozilla.org\\chrome.manifest": {"mincount": 2, "maxcount": 2, "minbytes": 600, "maxbytes": 600},
  "{profile}\\extensions\\talos-powers@mozilla.org\\chrome.manifest": {"mincount": 2, "maxcount": 2, "minbytes": 600, "maxbytes": 600},
  "{profile}\\extensions\\talos-powers@mozilla.org\\chrome\\talos-powers-content.js": {"mincount": 2, "maxcount": 2, "minbytes": 2000, "maxbytes": 2000},
  "{profile}\\mimetypes.rdf": {"mincount": 2, "maxcount": 2, "minbytes": 8192, "maxbytes": 8192},
  "{firefox}\\profiles.ini": {"mincount": 2, "maxcount": 2, "minbytes": 8192, "maxbytes": 8192},
  "c:\\program files\\nvidia corporation\\3d vision\\nvstereoapii.dll": {"mincount": 0, "maxcount": 4, "minbytes": 2, "maxbytes": 33792},
  "c:\\programdata\\nvidia corporation\\drs\\nvdrssel.bin": {"mincount": 2, "maxcount": 2, "minbytes": 2, "maxbytes": 2},
  "c:\\programdata\\nvidia corporation\\drs\\nvapptimestamps": {"mincount": 22, "maxcount": 22, "minbytes": 704, "maxbytes": 704},
  "{firefox}\\browser\\components\\components.manifest": {"mincount": 2, "maxcount": 2, "minbytes": 68, "maxbytes": 68},
- "{profile}\\places.sqlite": {"mincount": 8, "maxcount": 8, "minbytes": 196808, "maxbytes": 196808}, 
+ "{profile}\\places.sqlite": {"mincount": 8, "maxcount": 8, "minbytes": 196808, "maxbytes": 196808},
  "{profile}\\pluginreg.dat": {"mincount": 2, "maxcount": 2, "minbytes": 1892, "maxbytes": 1892},
  "{firefox}\\defaults\\pref\\channel-prefs.js": {"mincount": 4, "maxcount": 4, "minbytes": 1432, "maxbytes": 1432},
  "c:\\windows\\system32\\dwrite.dll": {"mincount": 4, "maxcount": 4, "minbytes": 16384, "maxbytes": 90112},
  "c:\\users\\desktop.ini": {"mincount": 2, "maxcount": 2, "minbytes": 352, "maxbytes": 352},
  "{desktop}\\desktop.ini": {"mincount": 6, "maxcount": 6, "minbytes": 1692, "maxbytes": 1692},
  "c:\\windows\\fonts\\staticcache.dat": {"mincount": 2, "maxcount": 2, "minbytes": 120, "maxbytes": 120},
  "c:\\windows\\system32\\spool\\drivers\\color\\srgb color space profile.icm": {"mincount": 2, "maxcount": 2, "minbytes": 8192, "maxbytes": 8192},
  "{profile}\\search.json": {"mincount": 12, "maxcount": 12, "minbytes": 33350, "maxbytes": 33350},