Bug 812898 Implement plugin preview overlay from Bug 776208 (combined patch for comm-beta) r=Neil a=Callek. SEAMONKEY_2_15b3_BUILD1 SEAMONKEY_2_15b3_RELEASE
authorPhilip Chee <philip.chee@gmail.com>
Tue, 20 Nov 2012 00:05:01 +0800
changeset 13981 3363831600dfd7f8c099ba68e62c08f554835dcb
parent 13978 7b0ff9c645ed782cb7e4d0cfee72a920a84fb6b7
child 13982 7398241a19fbf0d62376e3f0148084956a86ce10
child 13984 ef261d601f6df4dadcae4909309a7c2909bf4e01
push id787
push userphilip.chee@gmail.com
push dateSat, 01 Dec 2012 09:44:28 +0000
treeherdercomm-beta@3363831600df [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersNeil, Callek
bugs812898, 776208
Bug 812898 Implement plugin preview overlay from Bug 776208 (combined patch for comm-beta) r=Neil a=Callek.
suite/browser/test/Makefile.in
suite/browser/test/browser/browser_pluginnotification.js
suite/browser/test/browser/browser_pluginplaypreview.js
suite/browser/test/browser/head.js
suite/common/bindings/notification.xml
--- a/suite/browser/test/Makefile.in
+++ b/suite/browser/test/Makefile.in
@@ -30,16 +30,17 @@ ifneq (gtk2,$(MOZ_WIDGET_TOOLKIT))
 		subtst_contextmenu.html \
 		ctxmenu-image.png \
 		audio.ogg \
 		video.ogg \
 		$(NULL)
 endif
 
 _BROWSER_FILES = \
+                 head.js \
                  browser_bug329212.js \
                  browser_bug413915.js \
                  browser_bug427559.js \
                  browser_bug435325.js \
                  browser_bug519216.js \
                  browser_bug561636.js \
                  browser_bug562649.js \
                  browser_bug581947.js \
@@ -47,16 +48,17 @@ endif
                  browser_bug595507.js \
                  browser_bug623155.js \
                  browser_fayt.js \
                  browser_page_style_menu.js \
                  page_style_sample.html \
                  browser_pageInfo.js \
                  feed_tab.html \
                  browser_pluginnotification.js \
+                 browser_pluginplaypreview.js \
                  plugin_alternate_content.html \
                  plugin_bug743421.html \
                  plugin_bug749455.html \
                  plugin_clickToPlayAllow.html \
                  plugin_hidden_to_visible.html \
                  plugin_unknown.html \
                  plugin_test.html \
                  plugin_test2.html \
--- a/suite/browser/test/browser/browser_pluginnotification.js
+++ b/suite/browser/test/browser/browser_pluginnotification.js
@@ -11,47 +11,16 @@ var gClickToPlayPluginExpectedEvents = 5
 function count(o)
 {
   var n = 0;
   for (var p in o)
     n += Object.prototype.hasOwnProperty.call(o, p);
   return n;
 }
 
-
-function getTestPlugin() {
-  var ph = Components.classes["@mozilla.org/plugin/host;1"]
-                     .getService(Components.interfaces.nsIPluginHost);
-  var tags = ph.getPluginTags();
-
-  // Find the test plugin
-  for (var i = 0; i < tags.length; i++) {
-    if (tags[i].name == "Test Plug-in")
-      return tags[i];
-  }
-  ok(false, "Unable to find plugin");
-  return null;
-}
-
-
-function waitForCondition(condition, nextTest, errorMsg) {
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
-    }
-    if (condition()) {
-      moveOn();
-   }
-    tries++;
-  }, 100);
-  var moveOn = function() { clearInterval(interval); nextTest(); };
-}
-
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 // This listens for the next opened tab and checks it is of the right url.
 // opencallback is called when the new tab is fully loaded
 // closecallback is called when the tab is closed
 function TabOpenListener(url, opencallback, closecallback) {
   this.url = url;
   this.opencallback = opencallback;
new file mode 100644
--- /dev/null
+++ b/suite/browser/test/browser/browser_pluginplaypreview.js
@@ -0,0 +1,311 @@
+/* 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/. */
+
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir;
+
+var gTestBrowser = null;
+var gNextTest = null;
+var gNextTestSkip = 0;
+var gPlayPreviewPluginActualEvents = 0;
+var gPlayPreviewPluginExpectedEvents = 1;
+
+var gPlayPreviewRegistration = null;
+
+function registerPlayPreview(mimeType, targetUrl) {
+
+  function StreamConverterFactory() {}
+  StreamConverterFactory.prototype = {
+    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIFactory]),
+    _targetConstructor: null,
+
+    register: function register(targetConstructor) {
+      this._targetConstructor = targetConstructor;
+      var proto = targetConstructor.prototype;
+      var registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+      registrar.registerFactory(proto.classID, proto.classDescription,
+                                proto.contractID, this);
+    },
+
+    unregister: function unregister() {
+      var proto = this._targetConstructor.prototype;
+      var registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+      registrar.unregisterFactory(proto.classID, this);
+      this._targetConstructor = null;
+    },
+
+    // nsIFactory
+    createInstance: function createInstance(aOuter, iid) {
+      if (aOuter !== null)
+        throw Components.results.NS_ERROR_NO_AGGREGATION;
+      return (new (this._targetConstructor)).QueryInterface(iid);
+    },
+
+    // nsIFactory
+    lockFactory: function lockFactory(lock) {
+      // No longer used as of gecko 1.7.
+      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+    }
+  };
+
+  function OverlayStreamConverter() {}
+  OverlayStreamConverter.prototype = {
+    QueryInterface: XPCOMUtils.generateQI([
+        Components.interfaces.nsISupports,
+        Components.interfaces.nsIStreamConverter,
+        Components.interfaces.nsIStreamListener,
+        Components.interfaces.nsIRequestObserver
+    ]),
+
+    classID: Components.ID('{4c6030f7-e20a-264f-0f9b-ada3a9e97384}'),
+    classDescription: 'overlay-test-data Component',
+    contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*',
+
+    // nsIStreamConverter::convert
+    convert: function(aFromStream, aFromType, aToType, aCtxt) {
+      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+    },
+
+    // nsIStreamConverter::asyncConvertData
+    asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
+      var isValidRequest = false;
+      try {
+        var request = aCtxt;
+        request.QueryInterface(Components.interfaces.nsIChannel);
+        var spec = request.URI.spec;
+        var expectedSpec = 'data:application/x-moz-playpreview;,' + mimeType;
+        isValidRequest = (spec == expectedSpec);
+      } catch (e) { }
+      if (!isValidRequest)
+        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+
+      // Store the listener passed to us
+      this.listener = aListener;
+    },
+
+    // nsIStreamListener::onDataAvailable
+    onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
+      // Do nothing since all the data loading is handled by the viewer.
+      ok(false, "onDataAvailable should not be called");
+    },
+
+    // nsIRequestObserver::onStartRequest
+    onStartRequest: function(aRequest, aContext) {
+
+      // Setup the request so we can use it below.
+      aRequest.QueryInterface(Components.interfaces.nsIChannel);
+      // Cancel the request so the viewer can handle it.
+      aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+
+      // Create a new channel that is viewer loaded as a resource.
+      var ioService = Services.io;
+      var channel = ioService.newChannel(targetUrl, null, null);
+      channel.asyncOpen(this.listener, aContext);
+    },
+
+    // nsIRequestObserver::onStopRequest
+    onStopRequest: function(aRequest, aContext, aStatusCode) {
+      // Do nothing.
+    }
+  };
+
+  var ph = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+  ph.registerPlayPreviewMimeType(mimeType);
+
+  var factory = new StreamConverterFactory();
+  factory.register(OverlayStreamConverter);
+
+  return (gPlayPreviewRegistration = {
+    unregister: function() {
+      ph.unregisterPlayPreviewMimeType(mimeType);
+      factory.unregister();
+      gPlayPreviewRegistration = null;
+    }
+  });
+}
+
+function unregisterPlayPreview() {
+  gPlayPreviewRegistration.unregister();
+}
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+
+function test() {
+  waitForExplicitFinish();
+  registerCleanupFunction(function() {
+    if (gPlayPreviewRegistration)
+      gPlayPreviewRegistration.unregister();
+    Services.prefs.clearUserPref("plugins.click_to_play");
+  });
+
+  var newTab = gBrowser.addTab();
+  gBrowser.selectedTab = newTab;
+  gTestBrowser = gBrowser.selectedBrowser;
+  gTestBrowser.addEventListener("load", pageLoad, true);
+  gTestBrowser.addEventListener("PluginPlayPreview", handlePluginPlayPreview, true);
+
+  registerPlayPreview('application/x-test', 'about:');
+  prepareTest(test1a, gTestRoot + "plugin_test.html", 1);
+}
+
+function finishTest() {
+  gTestBrowser.removeEventListener("load", pageLoad, true);
+  gTestBrowser.removeEventListener("PluginPlayPreview", handlePluginPlayPreview, true);
+  gBrowser.removeCurrentTab();
+  window.focus();
+  finish();
+}
+
+function handlePluginPlayPreview() {
+  gPlayPreviewPluginActualEvents++;
+}
+
+function pageLoad() {
+  // The plugin events are async dispatched and can come after the load event
+  // This just allows the events to fire before we then go on to test the states
+
+  // iframe might triggers load event as well, making sure we skip some to let
+  // all iframes on the page be loaded as well
+  if (gNextTestSkip) {
+    gNextTestSkip--;
+    return;
+  }
+  executeSoon(gNextTest);
+}
+
+function prepareTest(nextTest, url, skip) {
+  gNextTest = nextTest;
+  gNextTestSkip = skip;
+  gTestBrowser.contentWindow.location = url;
+}
+
+// Tests a page with normal play preview registration (1/2)
+function test1a() {
+  var notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  ok(!notificationBox.getNotificationWithValue("missing-plugins"), "Test 1a, Should not have displayed the missing plugin notification");
+  ok(!notificationBox.getNotificationWithValue("blocked-plugins"), "Test 1a, Should not have displayed the blocked plugin notification");
+
+  var pluginInfo = getTestPlugin();
+  ok(pluginInfo, "Should have a test plugin");
+
+  var doc = gTestBrowser.contentDocument;
+  var plugin = doc.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  is(objLoadingContent.pluginFallbackType, Components.interfaces.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 1a, plugin fallback type should be PLUGIN_PLAY_PREVIEW");
+  ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated");
+
+  var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
+  ok(overlay, "Test 1a, the overlay div is expected");
+
+  var iframe = overlay.getElementsByClassName("previewPluginContentFrame")[0];
+  ok(iframe && iframe.localName == "iframe", "Test 1a, the overlay iframe is expected");
+  var iframeHref = iframe.contentWindow.location.href;
+  ok(iframeHref == "about:", "Test 1a, the overlay about: content is expected");
+
+  var rect = iframe.getBoundingClientRect();
+  ok(rect.width == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px width before being replaced by actual plugin");
+  ok(rect.height == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px height before being replaced by actual plugin");
+
+  var e = overlay.ownerDocument.createEvent("CustomEvent");
+  e.initCustomEvent("MozPlayPlugin", true, true, null);
+  overlay.dispatchEvent(e);
+  var condition = function() objLoadingContent.activated;
+  waitForCondition(condition, test1b, "Test 1a, Waited too long for plugin to stop play preview");
+}
+
+// Tests that activating via MozPlayPlugin through the notification works (part 2/2)
+function test1b() {
+  var plugin = gTestBrowser.contentDocument.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  ok(objLoadingContent.activated, "Test 1b, Plugin should be activated");
+
+  is(gPlayPreviewPluginActualEvents, gPlayPreviewPluginExpectedEvents,
+     "There should be exactly one PluginPlayPreview event");
+
+  unregisterPlayPreview();
+
+  prepareTest(test2, gTestRoot + "plugin_test.html");
+}
+
+// Tests a page with a working plugin in it -- the mime type was just unregistered.
+function test2() {
+  var plugin = gTestBrowser.contentDocument.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  ok(objLoadingContent.activated, "Test 2, Plugin should be activated");
+
+  registerPlayPreview('application/x-unknown', 'about:');
+
+  prepareTest(test3, gTestRoot + "plugin_test.html");
+}
+
+// Tests a page with a working plugin in it -- diffent play preview type is reserved.
+function test3() {
+  var plugin = gTestBrowser.contentDocument.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  ok(objLoadingContent.activated, "Test 3, Plugin should be activated");
+
+  unregisterPlayPreview();
+
+  registerPlayPreview('application/x-test', 'about:');
+  Services.prefs.setBoolPref("plugins.click_to_play", true);
+  prepareTest(test4a, gTestRoot + "plugin_test.html", 1);
+}
+
+// Test a fallback to the click-to-play
+function test4a() {
+  var doc = gTestBrowser.contentDocument;
+  var plugin = doc.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  is(objLoadingContent.pluginFallbackType, Components.interfaces.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 4a, plugin fallback type should be PLUGIN_PLAY_PREVIEW");
+  ok(!objLoadingContent.activated, "Test 4a, Plugin should not be activated");
+
+  var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
+  ok(overlay, "Test 4a, the overlay div is expected");
+
+  var e = overlay.ownerDocument.createEvent("CustomEvent");
+  e.initCustomEvent("MozPlayPlugin", true, true, true);
+  overlay.dispatchEvent(e);
+  var condition = function() objLoadingContent.pluginFallbackType == Components.interfaces.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
+  waitForCondition(condition, test4b, "Test 4a, Waited too long for plugin to stop play preview");
+}
+
+function test4b() {
+  var doc = gTestBrowser.contentDocument;
+  var plugin = doc.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  ok(objLoadingContent.pluginFallbackType != Components.interfaces.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 4b, plugin fallback type should not be PLUGIN_PLAY_PREVIEW");
+  ok(!objLoadingContent.activated, "Test 4b, Plugin should not be activated");
+
+  prepareTest(test5a, gTestRoot + "plugin_test.html", 1);
+}
+
+// Test a bypass of the click-to-play
+function test5a() {
+  var doc = gTestBrowser.contentDocument;
+  var plugin = doc.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  is(objLoadingContent.pluginFallbackType, Components.interfaces.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 5a, plugin fallback type should be PLUGIN_PLAY_PREVIEW");
+  ok(!objLoadingContent.activated, "Test 5a, Plugin should not be activated");
+
+  var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
+  ok(overlay, "Test 5a, the overlay div is expected");
+
+  var e = overlay.ownerDocument.createEvent("CustomEvent");
+  e.initCustomEvent("MozPlayPlugin", true, true, false);
+  overlay.dispatchEvent(e);
+  var condition = function() objLoadingContent.activated;
+  waitForCondition(condition, test5b, "Test 5a, Waited too long for plugin to stop play preview");
+}
+
+function test5b() {
+  var doc = gTestBrowser.contentDocument;
+  var plugin = doc.getElementById("test");
+  var objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+  ok(objLoadingContent.activated, "Test 5b, Plugin should be activated");
+
+  finishTest();
+}
+
new file mode 100644
--- /dev/null
+++ b/suite/browser/test/browser/head.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+function getTestPlugin() {
+  var ph = Components.classes["@mozilla.org/plugin/host;1"]
+                     .getService(Components.interfaces.nsIPluginHost);
+  var tags = ph.getPluginTags();
+
+  // Find the test plugin
+  for (var i = 0; i < tags.length; i++) {
+    if (tags[i].name == "Test Plug-in")
+      return tags[i];
+  }
+
+  ok(false, "Unable to find plugin");
+  return null;
+}
+
+function waitForCondition(condition, nextTest, errorMsg) {
+  var tries = 0;
+  var interval = setInterval(function() {
+    if (tries >= 30) {
+      ok(false, errorMsg);
+      moveOn();
+    }
+    if (condition()) {
+      moveOn();
+    }
+    tries++;
+  }, 100);
+  var moveOn = function() { clearInterval(interval); nextTest(); };
+}
--- a/suite/common/bindings/notification.xml
+++ b/suite/common/bindings/notification.xml
@@ -435,51 +435,79 @@
             // Now we can display our notification.
             const priority = this.PRIORITY_WARNING_MEDIUM;
             this.appendNotification(aMessage, aNotification,
                                     null, priority, aButtons);
           ]]>
         </body>
       </method>
 
+      <method name="canActivatePlugin">
+        <parameter name="objLoadingContent"/>
+        <body>
+          <![CDATA[
+            var nsIObjectLoadingContent = Components.interfaces.nsIObjectLoadingContent;
+            return !objLoadingContent.activated &&
+                   objLoadingContent.pluginFallbackType !== nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW;
+          ]]>
+        </body>
+      </method>
+
       <method name="activatePlugins">
         <body>
           <![CDATA[
             this.clickToPlayPluginsActivated = true;
             var plugins = this.contentWindowUtils.plugins;
             for (let plugin of plugins) {
               let objLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
-              if (!objLoadingContent.activated)
+              if (this.canActivatePlugin(objLoadingContent))
                 objLoadingContent.playPlugin();
             }
             this.removePluginClickToPlayPrompt();
           ]]>
         </body>
       </method>
       
       <method name="activateSinglePlugin">
         <parameter name="aPlugin"/>
         <body>
           <![CDATA[
             var objLoadingContent = aPlugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
-            if (!objLoadingContent.activated)
+            if (this.canActivatePlugin(objLoadingContent))
               objLoadingContent.playPlugin();
         
             var haveUnplayedPlugins = this.contentWindowUtils.plugins.some(function(plugin) {
               var hupObjLoadingContent = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
-              return plugin != aPlugin && !hupObjLoadingContent.activated;
-            });
+              return plugin != aPlugin && this.canActivatePlugin(hupObjLoadingContent);
+            }, this);
             if (!haveUnplayedPlugins) {
               this.removePluginClickToPlayPrompt();
               this.clickToPlayPromptShown = false;
             }
           ]]>
         </body>
       </method>
 
+      <method name="stopPlayPreview">
+        <parameter name="aPlugin"/>
+        <parameter name="aPlayPlugin"/>
+        <body>
+          <![CDATA[
+            var objLoadingContent = aPlugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
+            if (objLoadingContent.activated)
+              return;
+
+            if (aPlayPlugin)
+              objLoadingContent.playPlugin();
+            else
+              objLoadingContent.cancelPlayPreview();
+          ]]>
+        </body>
+      </method>
+
       <method name="openURLPref">
         <parameter name="aPref"/>
         <body>
           <![CDATA[
             var url = this._urlFormatter.formatURLPref(aPref);
             var nsIBrowserDOMWindow = Components.interfaces.nsIBrowserDOMWindow;
 
             var browserWin;
@@ -664,16 +692,41 @@
             link.setAttribute("value", this._stringBundle.GetStringFromName("crashedpluginsMessage.learnMore"));
             this.addLinkClickCallback(link, this.openHelpPage);
             var description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
             description.appendChild(link);
           ]]>
         </body>
       </method>
 
+      <method name="handleEvent">
+        <parameter name="aEvent"/>
+        <body>
+          <![CDATA[
+          if (!aEvent.isTrusted)
+            return;
+
+          if (aEvent.type != "MozPlayPlugin")
+            return;
+
+          var previewContent = aEvent.currentTarget;
+          previewContent.removeEventListener("MozPlayPlugin", this, true);
+
+          var pluginElement = previewContent.ownerDocument.getBindingParent(previewContent);
+          var playPlugin = !aEvent.detail;
+          this.stopPlayPreview(pluginElement, playPlugin);
+
+          // cleaning up: removes overlay iframe from the DOM
+          var iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+          if (iframe)
+            previewContent.removeChild(iframe);
+          ]]>
+        </body>
+      </method>
+
       <method name="installMissingPlugins">
         <body>
           <![CDATA[
             window.openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul",
                               "", "chrome,centerscreen,resizable=yes",
                               {plugins: this.missingPlugins, browser: this.activeBrowser});
           ]]>
         </body>
@@ -1083,16 +1136,51 @@
 
             let overlay = doc.getAnonymousElementByAttribute(pluginElement, "class", "mainBox");
             if (this.isTooSmall(pluginElement, overlay))
               overlay.style.visibility = "hidden";
           ]]>
         </body>
       </method>
 
+      <method name="handlePlayPreviewEvent">
+        <parameter name="pluginElement"/>
+        <body>
+          <![CDATA[
+            var doc = pluginElement.ownerDocument;
+            var previewContent = doc.getAnonymousElementByAttribute(pluginElement, "class", "previewPluginContent");
+            if (!previewContent) {
+            // If the XBL binding is not attached (element is display:none),
+            // fallback to click-to-play logic.
+              this.stopPlayPreview(pluginElement, false);
+              return;
+            }
+
+            var iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+            if (!iframe) {
+              // lazy initialization of the iframe
+              iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+              iframe.className = "previewPluginContentFrame";
+              previewContent.appendChild(iframe);
+
+              // Force a style flush, so that we ensure our binding is attached.
+              pluginElement.clientTop;
+            }
+
+            var pluginInfo = this.getPluginInfo(pluginElement);
+            var playPreviewUri = "data:application/x-moz-playpreview;," + pluginInfo.mimetype;
+            iframe.src = playPreviewUri;
+
+            // The MozPlayPlugin event can be dispatched from the extension chrome
+            // code to replace the preview content with the native plugin.
+            previewContent.addEventListener("MozPlayPlugin", this, true);
+          ]]>
+        </body>
+      </method>
+
       <method name="showGeolocationPrompt">
         <parameter name="file"/>
         <parameter name="site"/>
         <parameter name="allowCallback"/>
         <parameter name="cancelCallback"/>
         <body>
           <![CDATA[
             var type = "geolocation";
@@ -1905,16 +1993,22 @@
       </handler>
 
       <handler event="PluginClickToPlay" phase="capturing">
         <![CDATA[
           this.setupPluginClickToPlay(event.target);
         ]]>
       </handler>
 
+      <handler event="PluginPlayPreview" phase="capturing">
+        <![CDATA[
+          this.handlePlayPreviewEvent(event.target);
+        ]]>
+      </handler>
+
       <handler event="NewPluginInstalled" phase="capturing">
         <![CDATA[
           this.missingPlugins = {};
 
           // clean up the UI after a new plugin has been installed.
           var notification = this.getNotificationWithValue("missing-plugins");
           if (notification)
             this.removeNotification(notification);
@@ -1951,18 +2045,18 @@
           var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                              .getService(nsIPermissionManager);
           var pluginPermission = pm.testPermission(this.activeBrowser.currentURI, "plugins");
           if (pluginPermission == nsIPermissionManager.DENY_ACTION)
             return;
 
           var pluginNeedsActivation = this.contentWindowUtils.plugins.some(function(aPlugin) {
             var objLoadingContent = aPlugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent);
-            return !objLoadingContent.activated;
-          });
+            return this.canActivatePlugin(objLoadingContent);
+          }, this);
           if (pluginNeedsActivation)
             this.showPluginClickToPlayPrompt();
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="popup-notification"