From 7bc597d3c0ead4ae2a7568339e7a0ec0c901216e Mon Sep 17 00:00:00 2001 From: Chun-Min Chang Date: Thu, 10 Nov 2016 15:33:09 +0800 Subject: [PATCH] Bug 1289974 part 1: Device selection for presentation API on Firefox; r=mconley MozReview-Commit-ID: 8z8xM4hr2F3 --HG-- extra : rebase_source : 35c2b090b3d8711df3ee9a1edbf3fe3a7535199a --- browser/extensions/moz.build | 1 + browser/extensions/presentation/bootstrap.js | 84 ++++++ .../content/PresentationDevicePrompt.jsm | 254 ++++++++++++++++++ .../extensions/presentation/install.rdf.in | 33 +++ browser/extensions/presentation/jar.mn | 5 + .../locale/en-US/presentation.properties | 6 + browser/extensions/presentation/locale/jar.mn | 8 + .../extensions/presentation/locale/moz.build | 7 + browser/extensions/presentation/moz.build | 14 + .../presentation/skin/shared/link.svg | 24 ++ browser/locales/Makefile.in | 1 + 11 files changed, 437 insertions(+) create mode 100644 browser/extensions/presentation/bootstrap.js create mode 100644 browser/extensions/presentation/content/PresentationDevicePrompt.jsm create mode 100644 browser/extensions/presentation/install.rdf.in create mode 100644 browser/extensions/presentation/jar.mn create mode 100644 browser/extensions/presentation/locale/en-US/presentation.properties create mode 100644 browser/extensions/presentation/locale/jar.mn create mode 100644 browser/extensions/presentation/locale/moz.build create mode 100644 browser/extensions/presentation/moz.build create mode 100644 browser/extensions/presentation/skin/shared/link.svg diff --git a/browser/extensions/moz.build b/browser/extensions/moz.build index 9b01ed095241..1aef7d916f4a 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/moz.build @@ -17,4 +17,5 @@ if 'a' in CONFIG['GRE_MILESTONE']: DIRS += [ 'flyweb', 'formautofill', + 'presentation', ] diff --git a/browser/extensions/presentation/bootstrap.js b/browser/extensions/presentation/bootstrap.js new file mode 100644 index 000000000000..5cd2f11036b9 --- /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; + }, +}; diff --git a/browser/extensions/presentation/content/PresentationDevicePrompt.jsm b/browser/extensions/presentation/content/PresentationDevicePrompt.jsm new file mode 100644 index 000000000000..3ad80a4b7608 --- /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" 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; + }, +}; diff --git a/browser/extensions/presentation/install.rdf.in b/browser/extensions/presentation/install.rdf.in new file mode 100644 index 000000000000..431592fee161 --- /dev/null +++ b/browser/extensions/presentation/install.rdf.in @@ -0,0 +1,33 @@ + + + +#filter substitution + + + + + presentation@mozilla.org + 1.0.0 + 2 + true + true + + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + @MOZ_APP_VERSION@ + @MOZ_APP_MAXVERSION@ + + + + + Presentation + Discover nearby devices in the browser + + diff --git a/browser/extensions/presentation/jar.mn b/browser/extensions/presentation/jar.mn new file mode 100644 index 000000000000..dcc56c19accb --- /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/*) diff --git a/browser/extensions/presentation/locale/en-US/presentation.properties b/browser/extensions/presentation/locale/en-US/presentation.properties new file mode 100644 index 000000000000..0caf8e10d253 --- /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 diff --git a/browser/extensions/presentation/locale/jar.mn b/browser/extensions/presentation/locale/jar.mn new file mode 100644 index 000000000000..d07d3434a3ab --- /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/*) diff --git a/browser/extensions/presentation/locale/moz.build b/browser/extensions/presentation/locale/moz.build new file mode 100644 index 000000000000..3bbe6729759c --- /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'] diff --git a/browser/extensions/presentation/moz.build b/browser/extensions/presentation/moz.build new file mode 100644 index 000000000000..ade4f67ff275 --- /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'] diff --git a/browser/extensions/presentation/skin/shared/link.svg b/browser/extensions/presentation/skin/shared/link.svg new file mode 100644 index 000000000000..fdfb9602d19e --- /dev/null +++ b/browser/extensions/presentation/skin/shared/link.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/browser/locales/Makefile.in b/browser/locales/Makefile.in index 6b0455bf0285..d9ce78cde96e 100644 --- a/browser/locales/Makefile.in +++ b/browser/locales/Makefile.in @@ -100,6 +100,7 @@ libs-%: @$(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-$*