diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 33b5502192d1..6bf8969f2d7e 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -6248,7 +6248,7 @@ if (width != this.mTabstripWidth) { this.adjustTabstrip(); this._fillTrailingGap(); - this._handleTabSelect(); + this._handleTabSelect(false); this.mTabstripWidth = width; } break; diff --git a/browser/components/contextualidentity/moz.build b/browser/components/contextualidentity/moz.build index 62d333db8232..b2d25b5dffea 100644 --- a/browser/components/contextualidentity/moz.build +++ b/browser/components/contextualidentity/moz.build @@ -11,4 +11,4 @@ BROWSER_CHROME_MANIFESTS += [ JAR_MANIFESTS += ['jar.mn'] with Files('**'): - BUG_COMPONENT = ('Firefox', 'Contextual Identity') + BUG_COMPONENT = ('DOM', 'Security') diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index a663af3c0d3a..b239b26939df 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -67,6 +67,7 @@ support-files = [browser_ext_incognito_popup.js] [browser_ext_lastError.js] [browser_ext_omnibox.js] +skip-if = debug || asan # Bug 1354681 [browser_ext_optionsPage_browser_style.js] [browser_ext_optionsPage_privileges.js] [browser_ext_pageAction_context.js] diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml index 5282ba7c67b0..710f2608ff57 100644 --- a/browser/components/search/content/search.xml +++ b/browser/components/search/content/search.xml @@ -1225,7 +1225,7 @@ - + with:" header. this._updateAfterQueryChanged(); - let list = document.getAnonymousElementByAttribute(this, "anonid", - "search-panel-one-offs"); - // Handle opensearch items. This needs to be done before building the // list of one off providers, as that code will return early if all the // alternative engines are hidden. - this._rebuildAddEngineList(); + // Skip this in compact mode, ie. for the urlbar. + if (!this.compact) + this._rebuildAddEngineList(); + // Check if the one-off buttons really need to be rebuilt. + if (this._textbox) { + // We can't get a reliable value for the popup width without flushing, + // but the popup width won't change if the textbox width doesn't. + let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let textboxWidth = + DOMUtils.getBoundsWithoutFlushing(this._textbox).width; + // We can return early if neither the list of engines nor the panel + // width has changed. + if (this._engines && this._textboxWidth == textboxWidth) { + return; + } + this._textboxWidth = textboxWidth; + } + + let list = document.getAnonymousElementByAttribute(this, "anonid", + "search-panel-one-offs"); let settingsButton = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact"); @@ -1516,24 +1571,15 @@ if (settingsButton.nextSibling) settingsButton.nextSibling.remove(); - let Preferences = - Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences; - let pref = Preferences.get("browser.search.hiddenOneOffs"); - let hiddenList = pref ? pref.split(",") : []; - - let currentEngineName = Services.search.currentEngine.name; - let includeCurrentEngine = this.getAttribute("includecurrentengine"); - let engines = Services.search.getVisibleEngines().filter(e => { - return (includeCurrentEngine || e.name != currentEngineName) && - !hiddenList.includes(e.name); - }); + let engines = this.engines; + let oneOffCount = engines.length; let header = document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header") // header is a xul:deck so collapsed doesn't work on it, see bug 589569. - header.hidden = list.collapsed = !engines.length; + header.hidden = list.collapsed = !oneOffCount; - if (!engines.length) + if (!oneOffCount) return; let panelWidth = parseInt(this.popup.clientWidth); @@ -1547,7 +1593,6 @@ // If the tag with the list of search engines doesn't have // a fixed height, the panel will be sized incorrectly, causing the bottom // of the suggestion to be hidden. - let oneOffCount = engines.length; if (this.compact) ++oneOffCount; let rowCount = Math.ceil(oneOffCount / enginesPerRow); @@ -1647,20 +1692,12 @@ } // Add a button for each engine that the page in the selected browser - // offers, but with the following exceptions: - // - // (1) Not when the one-offs are compact. Compact one-offs are shown in - // the urlbar, and the add-engine buttons span the width of the popup, - // so if we added all the engines that a page offers, it could break the - // urlbar popup by offering a ton of engines. We should probably make a - // smaller version of the buttons for compact one-offs. - // - // (2) Not when there are too many offered engines. The popup isn't - // designed to handle too many (by scrolling for example), so a page - // could break the popup by offering too many. Instead, add a single - // menu button with a submenu of all the engines. + // offers, except when there are too many offered engines. + // The popup isn't designed to handle too many (by scrolling for + // example), so a page could break the popup by offering too many. + // Instead, add a single menu button with a submenu of all the engines. - if (this.compact || !gBrowser.selectedBrowser.engines) { + if (!gBrowser.selectedBrowser.engines) { return; } diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini index 4ed0ba871e7c..214c9eabe10a 100644 --- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -96,6 +96,6 @@ skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5 [browser_two_tabs.js] # We want these tests to run for mochitest-dt as well, so we include them here: [../../../../browser/base/content/test/static/browser_parsable_css.js] -skip-if = debug # no point in running on both opt and debug, and will likely intermittently timeout on debug +skip-if = debug || asan # no point in running on both opt and debug, and will likely intermittently timeout on debug [../../../../browser/base/content/test/static/browser_all_files_referenced.js] -skip-if = debug # no point in running on both opt and debug, and will likely intermittently timeout on debug +skip-if = debug || asan # no point in running on both opt and debug, and will likely intermittently timeout on debug diff --git a/devtools/client/inspector/boxmodel/components/BoxModelProperties.js b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js index cad8dfd62a01..263333c8601d 100644 --- a/devtools/client/inspector/boxmodel/components/BoxModelProperties.js +++ b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js @@ -60,20 +60,14 @@ module.exports = createClass({ className: "boxmodel-properties-header", onDoubleClick: this.onToggleExpander, }, - dom.div( + dom.span( { className: "boxmodel-properties-expander theme-twisty", open: this.state.isOpen, onClick: this.onToggleExpander, } ), - dom.span( - { - className: "boxmodel-properties-label", - title: BOXMODEL_L10N.getStr("boxmodel.propertiesLabel"), - }, - BOXMODEL_L10N.getStr("boxmodel.propertiesLabel") - ) + BOXMODEL_L10N.getStr("boxmodel.propertiesLabel") ), dom.div( { diff --git a/devtools/client/inspector/grids/components/GridDisplaySettings.js b/devtools/client/inspector/grids/components/GridDisplaySettings.js index 05e96a57595c..29b3bf6b15d5 100644 --- a/devtools/client/inspector/grids/components/GridDisplaySettings.js +++ b/devtools/client/inspector/grids/components/GridDisplaySettings.js @@ -56,7 +56,9 @@ module.exports = createClass({ dom.ul( {}, dom.li( - {}, + { + className: "grid-settings-item", + }, dom.label( {}, dom.input( @@ -70,7 +72,9 @@ module.exports = createClass({ ) ), dom.li( - {}, + { + className: "grid-settings-item", + }, dom.label( {}, dom.input( diff --git a/devtools/client/inspector/grids/components/GridItem.js b/devtools/client/inspector/grids/components/GridItem.js index 90fd333d42f6..02f062ccbd5c 100644 --- a/devtools/client/inspector/grids/components/GridItem.js +++ b/devtools/client/inspector/grids/components/GridItem.js @@ -118,9 +118,7 @@ module.exports = createClass({ let { nodeFront } = grid; return dom.li( - { - className: "grid-item", - }, + {}, dom.label( {}, dom.input( diff --git a/devtools/client/inspector/layout/components/App.js b/devtools/client/inspector/layout/components/App.js index 184bb7b53714..3f27a3e784f7 100644 --- a/devtools/client/inspector/layout/components/App.js +++ b/devtools/client/inspector/layout/components/App.js @@ -52,6 +52,7 @@ const App = createClass({ return dom.div( { id: "layout-container", + className: "devtools-monospace", }, Accordion({ items: [ diff --git a/devtools/client/themes/boxmodel.css b/devtools/client/themes/boxmodel.css index 3765c60fb233..feaf123d5c2b 100644 --- a/devtools/client/themes/boxmodel.css +++ b/devtools/client/themes/boxmodel.css @@ -8,6 +8,7 @@ .boxmodel-container { overflow: auto; + padding-bottom: 4px; } /* Header */ @@ -112,7 +113,8 @@ .boxmodel-main > p > span, .boxmodel-main > p > input, -.boxmodel-content { +.boxmodel-content, +.boxmodel-size > span { vertical-align: middle; pointer-events: auto; } @@ -314,12 +316,16 @@ /* Box Model Properties: contains a list of relevant box model properties */ .boxmodel-properties-header { - display: flex; - padding: 2px 0; + padding: 2px 3px; } -.boxmodel-properties-wrapper { - padding: 0 9px; +.boxmodel-properties-expander { + vertical-align: middle; + display: inline-block; +} + +.boxmodel-properties-wrapper .property-view { + padding-inline-start: 17px; } .boxmodel-properties-wrapper .property-name-container { diff --git a/devtools/client/themes/layout.css b/devtools/client/themes/layout.css index 63f8e7e64a30..8052662f8a96 100644 --- a/devtools/client/themes/layout.css +++ b/devtools/client/themes/layout.css @@ -16,13 +16,15 @@ .grid-container { display: flex; flex-direction: column; - flex: 1; + flex: 1 auto; align-items: center; + min-width: 140px; } .grid-container > span { - font-weight: bold; - margin-bottom: 3px; + font-weight: 600; + margin-bottom: 5px; + pointer-events: none; } .grid-container > ul { @@ -32,9 +34,20 @@ } .grid-container li { + display: flex; + align-items: center; padding: 4px 0; } +.grid-container input { + margin: 0 5px; +} + +.grid-container label { + display: flex; + align-items: center; +} + /** * Grid Container */ @@ -45,13 +58,28 @@ margin: 5px; } +/** + * Grid Outline + */ + +.grid-outline { + margin-top: 10px; + overflow: visible; +} + /** * Grid Content */ .grid-content { display: flex; + flex-wrap: wrap; flex: 1; + margin-top: 10px; +} + +.grid-container:first-child { + margin-bottom: 10px; } /** @@ -97,15 +125,6 @@ * Grid Item */ -.grid-item { - display: flex; - align-items: center; -} - -.grid-item input { - margin: 0 5px; -} - .grid-color-swatch { width: 12px; height: 12px; @@ -118,3 +137,11 @@ .grid-color-value { display: none; } + +/** + * Settings Item + */ + +.grid-settings-item label { + line-height: 16px; +} diff --git a/devtools/server/actors/worker.js b/devtools/server/actors/worker.js index 60f0ed5865e4..2efc7cd896dc 100644 --- a/devtools/server/actors/worker.js +++ b/devtools/server/actors/worker.js @@ -355,6 +355,19 @@ protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, { "resource://devtools/server/service-worker-child.js", true); _serviceWorkerProcessScriptLoaded = true; } + + // XXX: Send the permissions down to the content process before starting + // the service worker within the content process. As we don't know what + // content process we're starting the service worker in (as we're using a + // broadcast channel to talk to it), we just broadcast the permissions to + // everyone as well. + // + // This call should be replaced with a proper implementation when + // ServiceWorker debugging is improved to support multiple content processes + // correctly. + Services.perms.broadcastPermissionsForPrincipalToAllContentProcesses( + this._registration.principal); + Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", { scope: this._registration.scope }); diff --git a/dom/base/CustomElementRegistry.cpp b/dom/base/CustomElementRegistry.cpp index c3dbdb492096..dcb9a6a6f997 100644 --- a/dom/base/CustomElementRegistry.cpp +++ b/dom/base/CustomElementRegistry.cpp @@ -550,15 +550,6 @@ CustomElementRegistry::Define(const nsAString& aName, const ElementDefinitionOptions& aOptions, ErrorResult& aRv) { - // We do this for [CEReaction] temporarily and it will be removed - // after webidl supports [CEReaction] annotation in bug 1309147. - DocGroup* docGroup = mWindow->GetDocGroup(); - if (!docGroup) { - aRv.Throw(NS_ERROR_UNEXPECTED); - return; - } - - AutoCEReaction ceReaction(docGroup->CustomElementReactionsStack()); aRv.MightThrowJSException(); AutoJSAPI jsapi; @@ -918,7 +909,11 @@ CustomElementReactionsStack::PopAndInvokeElementQueue() "Reaction stack shouldn't be empty"); ElementQueue& elementQueue = mReactionsStack.LastElement(); - InvokeReactions(elementQueue); + // Check element queue size in order to reduce function call overhead. + if (!elementQueue.IsEmpty()) { + InvokeReactions(elementQueue); + } + DebugOnly isRemovedElement = mReactionsStack.RemoveElement(elementQueue); MOZ_ASSERT(isRemovedElement, "Reaction stack should have an element queue to remove"); @@ -967,7 +962,10 @@ CustomElementReactionsStack::Enqueue(Element* aElement, void CustomElementReactionsStack::InvokeBackupQueue() { - InvokeReactions(mBackupQueue); + // Check backup queue size in order to reduce function call overhead. + if (!mBackupQueue.IsEmpty()) { + InvokeReactions(mBackupQueue); + } } void diff --git a/dom/base/CustomElementRegistry.h b/dom/base/CustomElementRegistry.h index da9dc3921e6e..5ab4fb89a3e7 100644 --- a/dom/base/CustomElementRegistry.h +++ b/dom/base/CustomElementRegistry.h @@ -180,7 +180,8 @@ public: // nsWeakPtr is a weak pointer of Element // The element reaction queues are stored in ElementReactionQueueMap. // We need to lookup ElementReactionQueueMap again to get relevant reaction queue. - typedef nsTArray ElementQueue; + // The choice of 1 for the auto size here is based on gut feeling. + typedef AutoTArray ElementQueue; /** * Enqueue a custom element upgrade reaction @@ -202,13 +203,19 @@ public: private: ~CustomElementReactionsStack() {}; - typedef nsTArray> ReactionQueue; + // There is 1 reaction in reaction queue, when 1) it becomes disconnected, + // 2) it’s adopted into a new document, 3) its attributes are changed, + // appended, removed, or replaced. + // There are 3 reactions in reaction queue when doing upgrade operation, + // e.g., create an element, insert a node. + typedef AutoTArray, 3> ReactionQueue; typedef nsClassHashtable ElementReactionQueueMap; ElementReactionQueueMap mElementReactionQueueMap; - nsTArray mReactionsStack; + // The choice of 8 for the auto size here is based on gut feeling. + AutoTArray mReactionsStack; ElementQueue mBackupQueue; // https://html.spec.whatwg.org/#enqueue-an-element-on-the-appropriate-element-queue bool mIsBackupQueueProcessing; diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index c9748fceac9e..22ec8b5df82f 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -6140,6 +6140,7 @@ nsDocument::RegisterElement(JSContext* aCx, const nsAString& aType, return; } + AutoCEReaction ceReaction(this->GetDocGroup()->CustomElementReactionsStack()); // Unconditionally convert TYPE to lowercase. nsAutoString lcType; nsContentUtils::ASCIIToLower(aType, lcType); diff --git a/dom/bindings/BindingUtils.cpp b/dom/bindings/BindingUtils.cpp index 2b81de28ddcc..90dac87d2cf7 100644 --- a/dom/bindings/BindingUtils.cpp +++ b/dom/bindings/BindingUtils.cpp @@ -60,6 +60,7 @@ #include "nsDOMClassInfo.h" #include "ipc/ErrorIPCUtils.h" #include "mozilla/UseCounter.h" +#include "mozilla/dom/DocGroup.h" namespace mozilla { namespace dom { @@ -3397,6 +3398,28 @@ GetDesiredProto(JSContext* aCx, const JS::CallArgs& aCallArgs, return true; } +CustomElementReactionsStack* +GetCustomElementReactionsStack(JS::Handle aObj) +{ + // This might not be the right object, if there are wrappers. Unwrap if we can. + JSObject* obj = js::CheckedUnwrap(aObj); + if (!obj) { + return nullptr; + } + + nsGlobalWindow* window = xpc::WindowGlobalOrNull(obj); + if (!window) { + return nullptr; + } + + DocGroup* docGroup = window->AsInner()->GetDocGroup(); + if (!docGroup) { + return nullptr; + } + + return docGroup->CustomElementReactionsStack(); +} + // https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor already_AddRefed CreateHTMLElement(const GlobalObject& aGlobal, const JS::CallArgs& aCallArgs, diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h index a94402552f4d..e41e69b45b24 100644 --- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -49,6 +49,7 @@ namespace mozilla { enum UseCounter : int16_t; namespace dom { +class CustomElementReactionsStack; template class Record; nsresult @@ -3196,6 +3197,12 @@ bool GetDesiredProto(JSContext* aCx, const JS::CallArgs& aCallArgs, JS::MutableHandle aDesiredProto); +// Get the CustomElementReactionsStack for the docgroup of the global +// of the underlying object of aObj. This can be null if aObj can't +// be CheckUnwrapped, or if the global of the result has no docgroup +// (e.g. because it's not a Window global). +CustomElementReactionsStack* +GetCustomElementReactionsStack(JS::Handle aObj); // This function is expected to be called from the constructor function for an // HTML element interface; the global/callargs need to be whatever was passed to // that constructor function. diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index df760388e9f1..899e7bd98b2c 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -1691,6 +1691,10 @@ DOMInterfaces = { 'register': False, }, +'TestCEReactionsInterface' : { + 'headerFile': 'TestBindingHeader.h', + 'register': False, + }, } # These are temporary, until they've been converted to use new DOM bindings diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index ec94ecbfc4ab..4514b2696a5a 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -7502,7 +7502,7 @@ class CGPerSignatureCall(CGThing): def __init__(self, returnType, arguments, nativeMethodName, static, descriptor, idlNode, argConversionStartsAt=0, getter=False, setter=False, isConstructor=False, useCounterName=None, - resultVar=None): + resultVar=None, objectName="obj"): assert idlNode.isMethod() == (not getter and not setter) assert idlNode.isAttr() == (getter or setter) # Constructors are always static @@ -7701,6 +7701,17 @@ class CGPerSignatureCall(CGThing): CGIfWrapper(CGList(xraySteps), "objIsXray")) + if (idlNode.getExtendedAttribute('CEReactions') is not None and + not getter): + cgThings.append(CGGeneric(fill( + """ + CustomElementReactionsStack* reactionsStack = GetCustomElementReactionsStack(${obj}); + Maybe ceReaction; + if (reactionsStack) { + ceReaction.emplace(reactionsStack); + } + """, obj=objectName))) + # If this is a method that was generated by a maplike/setlike # interface, use the maplike/setlike generator to fill in the body. # Otherwise, use CGCallGenerator to call the native method. @@ -11208,7 +11219,8 @@ class CGProxySpecialOperation(CGPerSignatureCall): # CGPerSignatureCall won't do any argument conversion of its own. CGPerSignatureCall.__init__(self, returnType, arguments, nativeName, False, descriptor, operation, - len(arguments), resultVar=resultVar) + len(arguments), resultVar=resultVar, + objectName="proxy") if operation.isSetter() or operation.isCreator(): # arguments[0] is the index or name of the item that we're setting. @@ -13996,12 +14008,18 @@ class CGBindingRoot(CGThing): iface = desc.interface return any(m.getExtendedAttribute("Deprecated") for m in iface.members + [iface]) + def descriptorHasCEReactions(desc): + iface = desc.interface + return any(m.getExtendedAttribute("CEReactions") for m in iface.members + [iface]) + bindingHeaders["nsIDocument.h"] = any( descriptorDeprecated(d) for d in descriptors) bindingHeaders["mozilla/Preferences.h"] = any( descriptorRequiresPreferences(d) for d in descriptors) bindingHeaders["mozilla/dom/DOMJSProxyHandler.h"] = any( d.concrete and d.proxy for d in descriptors) + bindingHeaders["mozilla/dom/CustomElementRegistry.h"] = any( + descriptorHasCEReactions(d) for d in descriptors) def descriptorHasChromeOnly(desc): ctor = desc.interface.ctor() diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py index ad713e4e7cd8..2c6f7be3f570 100644 --- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -4097,6 +4097,11 @@ class IDLAttribute(IDLInterfaceMember): raise WebIDLError("Attribute returns a type that is not exposed " "everywhere where the attribute is exposed", [self.location]) + if self.getExtendedAttribute("CEReactions"): + if self.readonly: + raise WebIDLError("[CEReactions] is not allowed on " + "readonly attributes", + [self.location]) def handleExtendedAttribute(self, attr): identifier = attr.identifier() @@ -4283,6 +4288,10 @@ class IDLAttribute(IDLInterfaceMember): raise WebIDLError("[Unscopable] is only allowed on non-static " "attributes and operations", [attr.location, self.location]) + elif identifier == "CEReactions": + if not attr.noArguments(): + raise WebIDLError("[CEReactions] must take no arguments", + [attr.location]) elif (identifier == "Pref" or identifier == "Deprecated" or identifier == "SetterThrows" or @@ -5016,6 +5025,15 @@ class IDLMethod(IDLInterfaceMember, IDLScope): raise WebIDLError("[Unscopable] is only allowed on non-static " "attributes and operations", [attr.location, self.location]) + elif identifier == "CEReactions": + if not attr.noArguments(): + raise WebIDLError("[CEReactions] must take no arguments", + [attr.location]) + + if self.isSpecial() and not self.isSetter() and not self.isDeleter(): + raise WebIDLError("[CEReactions] is only allowed on operation, " + "attribute, setter, and deleter", + [attr.location, self.location]) elif (identifier == "Throws" or identifier == "CanOOM" or identifier == "NewObject" or diff --git a/dom/bindings/parser/tests/test_cereactions.py b/dom/bindings/parser/tests/test_cereactions.py new file mode 100644 index 000000000000..2f9397d903e5 --- /dev/null +++ b/dom/bindings/parser/tests/test_cereactions.py @@ -0,0 +1,162 @@ +def WebIDLTest(parser, harness): + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions(DOMString a)] void foo(boolean arg2); + }; + """) + + results = parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown for [CEReactions] with an argument") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions(DOMString b)] readonly attribute boolean bar; + }; + """) + + results = parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown for [CEReactions] with an argument") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] attribute boolean bar; + }; + """) + + results = parser.finish() + except Exception, e: + harness.ok(False, "Shouldn't have thrown for [CEReactions] used on writable attribute. %s" % e) + threw = True + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] void foo(boolean arg2); + }; + """) + + results = parser.finish() + except Exception, e: + harness.ok(False, "Shouldn't have thrown for [CEReactions] used on regular operations. %s" % e) + threw = True + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] readonly attribute boolean A; + }; + """) + + results = parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown for [CEReactions] used on a readonly attribute") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + [CEReactions] + interface Foo { + } + """) + + results = parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown for [CEReactions] used on a interface") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] getter any(DOMString name); + }; + """) + results = parser.finish() + except: + threw = True + + harness.ok(threw, + "Should have thrown for [CEReactions] used on a named getter") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] creator boolean (DOMString name, boolean value); + }; + """) + results = parser.finish() + except: + threw = True + + harness.ok(threw, + "Should have thrown for [CEReactions] used on a named creator") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] legacycaller double compute(double x); + }; + """) + results = parser.finish() + except: + threw = True + + harness.ok(threw, + "Should have thrown for [CEReactions] used on a legacycaller") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] stringifier DOMString (); + }; + """) + results = parser.finish() + except: + threw = True + + harness.ok(threw, + "Should have thrown for [CEReactions] used on a stringifier") + + parser = parser.reset() + threw = False + try: + parser.parse(""" + interface Foo { + [CEReactions] jsonifier; + }; + """) + + results = parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown for [CEReactions] used on a jsonifier") diff --git a/dom/bindings/test/TestBindingHeader.h b/dom/bindings/test/TestBindingHeader.h index c0f059125d9f..9b23ecb928fe 100644 --- a/dom/bindings/test/TestBindingHeader.h +++ b/dom/bindings/test/TestBindingHeader.h @@ -964,6 +964,11 @@ public: void NeedsCallerTypeMethod(CallerType); bool NeedsCallerTypeAttr(CallerType); void SetNeedsCallerTypeAttr(bool, CallerType); + void CeReactionsMethod(); + void CeReactionsMethodOverload(); + void CeReactionsMethodOverload(const nsAString&); + bool CeReactionsAttr() const; + void SetCeReactionsAttr(bool); int16_t LegacyCall(const JS::Value&, uint32_t, TestInterface&); void PassArgsWithDefaults(JSContext*, const Optional&, TestInterface*, const Dict&, double, @@ -1456,6 +1461,25 @@ public: virtual nsISupports* GetParentObject(); }; +class TestCEReactionsInterface : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_ISUPPORTS + + // We need a GetParentObject to make binding codegen happy + virtual nsISupports* GetParentObject(); + + int32_t Item(uint32_t); + uint32_t Length() const; + int32_t IndexedGetter(uint32_t, bool &); + void IndexedSetter(uint32_t, int32_t); + void NamedDeleter(const nsAString&, bool &); + void NamedGetter(const nsAString&, bool &, nsString&); + void NamedSetter(const nsAString&, const nsAString&); + void GetSupportedNames(nsTArray&); +}; + } // namespace dom } // namespace mozilla diff --git a/dom/bindings/test/TestCodeGen.webidl b/dom/bindings/test/TestCodeGen.webidl index 40387c5ed4c2..4259ca75641e 100644 --- a/dom/bindings/test/TestCodeGen.webidl +++ b/dom/bindings/test/TestCodeGen.webidl @@ -957,6 +957,10 @@ interface TestInterface { [NeedsSubjectPrincipal] attribute boolean needsSubjectPrincipalAttr; [NeedsCallerType] void needsCallerTypeMethod(); [NeedsCallerType] attribute boolean needsCallerTypeAttr; + [CEReactions] void ceReactionsMethod(); + [CEReactions] void ceReactionsMethodOverload(); + [CEReactions] void ceReactionsMethodOverload(DOMString bar); + [CEReactions] attribute boolean ceReactionsAttr; legacycaller short(unsigned long arg1, TestInterface arg2); void passArgsWithDefaults(optional long arg1, optional TestInterface? arg2 = null, @@ -1290,3 +1294,12 @@ interface TestWorkerExposedInterface { [HTMLConstructor] interface TestHTMLConstructorInterface { }; + +interface TestCEReactionsInterface { + [CEReactions] setter creator void (unsigned long index, long item); + [CEReactions] setter creator void (DOMString name, DOMString item); + [CEReactions] deleter void (DOMString name); + getter long item(unsigned long index); + getter DOMString (DOMString name); + readonly attribute unsigned long length; +}; diff --git a/dom/bindings/test/TestExampleGen.webidl b/dom/bindings/test/TestExampleGen.webidl index 255e93d9bb11..e2a04bc7a606 100644 --- a/dom/bindings/test/TestExampleGen.webidl +++ b/dom/bindings/test/TestExampleGen.webidl @@ -785,6 +785,10 @@ interface TestExampleInterface { [NeedsSubjectPrincipal] attribute boolean needsSubjectPrincipalAttr; [NeedsCallerType] void needsCallerTypeMethod(); [NeedsCallerType] attribute boolean needsCallerTypeAttr; + [CEReactions] void ceReactionsMethod(); + [CEReactions] void ceReactionsMethodOverload(); + [CEReactions] void ceReactionsMethodOverload(DOMString bar); + [CEReactions] attribute boolean ceReactionsAttr; legacycaller short(unsigned long arg1, TestInterface arg2); void passArgsWithDefaults(optional long arg1, optional TestInterface? arg2 = null, diff --git a/dom/bindings/test/TestJSImplGen.webidl b/dom/bindings/test/TestJSImplGen.webidl index 980c422df4f4..d729f2239b2b 100644 --- a/dom/bindings/test/TestJSImplGen.webidl +++ b/dom/bindings/test/TestJSImplGen.webidl @@ -801,6 +801,10 @@ interface TestJSImplInterface { [CanOOM] attribute boolean canOOMAttr; [GetterCanOOM] attribute boolean canOOMGetterAttr; [SetterCanOOM] attribute boolean canOOMSetterAttr; + [CEReactions] void ceReactionsMethod(); + [CEReactions] void ceReactionsMethodOverload(); + [CEReactions] void ceReactionsMethodOverload(DOMString bar); + [CEReactions] attribute boolean ceReactionsAttr; // NeedsSubjectPrincipal not supported on JS-implemented things for // now, because we always pass in the caller principal anyway. // [NeedsSubjectPrincipal] void needsSubjectPrincipalMethod(); diff --git a/dom/console/Console.cpp b/dom/console/Console.cpp index a26e13d6c997..832bd4cf834a 100644 --- a/dom/console/Console.cpp +++ b/dom/console/Console.cpp @@ -732,6 +732,9 @@ private: MOZ_ASSERT(argumentsValue.isObject()); JS::Rooted argumentsObj(aCx, &argumentsValue.toObject()); + if (NS_WARN_IF(!argumentsObj)) { + return; + } uint32_t length; if (!JS_GetArrayLength(aCx, argumentsObj, &length)) { @@ -1010,6 +1013,8 @@ Console::TimeStamp(const GlobalObject& aGlobal, { JSContext* cx = aGlobal.Context(); + ClearException ce(cx); + Sequence data; SequenceRooter rooter(cx, &data); @@ -1306,10 +1311,13 @@ Console::MethodInternal(JSContext* aCx, MethodName aMethodName, ? JS_GetEmptyStringValue(aCx) : aData[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return; + } nsAutoJSString key; - if (jsString) { - key.init(aCx, jsString); + if (!key.init(aCx, jsString)) { + return; } timelines->AddMarkerForDocShell(docShell, Move( @@ -1319,16 +1327,19 @@ Console::MethodInternal(JSContext* aCx, MethodName aMethodName, else if (isTimelineRecording && aData.Length() == 1) { JS::Rooted value(aCx, aData[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); - - if (jsString) { - nsAutoJSString key; - if (key.init(aCx, jsString)) { - timelines->AddMarkerForDocShell(docShell, Move( - MakeUnique( - key, aMethodName == MethodTime ? MarkerTracingType::START - : MarkerTracingType::END))); - } + if (!jsString) { + return; } + + nsAutoJSString key; + if (!key.init(aCx, jsString)) { + return; + } + + timelines->AddMarkerForDocShell(docShell, Move( + MakeUnique( + key, aMethodName == MethodTime ? MarkerTracingType::START + : MarkerTracingType::END))); } } else { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); diff --git a/dom/events/ContentEventHandler.cpp b/dom/events/ContentEventHandler.cpp index 8c67a8b0a763..ca921292027a 100644 --- a/dom/events/ContentEventHandler.cpp +++ b/dom/events/ContentEventHandler.cpp @@ -2164,7 +2164,7 @@ ContentEventHandler::OnQueryTextRect(WidgetQueryContentEvent* aEvent) } // Look for the last frame which should be included text rects. - ErrorResult erv; + IgnoredErrorResult erv; range->SelectNodeContents(*mRootContent, erv); if (NS_WARN_IF(erv.Failed())) { return NS_ERROR_UNEXPECTED; diff --git a/dom/file/ipc/Blob.cpp b/dom/file/ipc/Blob.cpp index ca19045bf711..7fbda72b8bf1 100644 --- a/dom/file/ipc/Blob.cpp +++ b/dom/file/ipc/Blob.cpp @@ -627,7 +627,9 @@ SerializeInputStreamInChunks(nsIInputStream* aInputStream, uint64_t aLength, MOZ_ASSERT(aInputStream); PMemoryStreamChild* child = aManager->SendPMemoryStreamConstructor(aLength); - MOZ_ASSERT(child); + if (NS_WARN_IF(!child)) { + return nullptr; + } const uint64_t kMaxChunk = 1024 * 1024; @@ -658,7 +660,9 @@ SerializeInputStreamInChunks(nsIInputStream* aInputStream, uint64_t aLength, return nullptr; } - child->SendAddChunk(buffer); + if (NS_WARN_IF(!child->SendAddChunk(buffer))) { + return nullptr; + } } return child; diff --git a/dom/webidl/CustomElementRegistry.webidl b/dom/webidl/CustomElementRegistry.webidl index dff612174386..788b6a4ed0dd 100644 --- a/dom/webidl/CustomElementRegistry.webidl +++ b/dom/webidl/CustomElementRegistry.webidl @@ -5,7 +5,7 @@ // https://html.spec.whatwg.org/#dom-window-customelements [Func="CustomElementRegistry::IsCustomElementEnabled"] interface CustomElementRegistry { - [Throws] + [CEReactions, Throws] void define(DOMString name, Function functionConstructor, optional ElementDefinitionOptions options); any get(DOMString name); diff --git a/extensions/cookie/nsPermissionManager.cpp b/extensions/cookie/nsPermissionManager.cpp index 71af2ffc40ea..24a3c4d986a5 100644 --- a/extensions/cookie/nsPermissionManager.cpp +++ b/extensions/cookie/nsPermissionManager.cpp @@ -3089,3 +3089,16 @@ nsPermissionManager::GetAllKeysForPrincipal(nsIPrincipal* aPrincipal) "Every principal should have at least one key."); return keys; } + +NS_IMETHODIMP +nsPermissionManager::BroadcastPermissionsForPrincipalToAllContentProcesses(nsIPrincipal* aPrincipal) +{ + nsTArray cps; + ContentParent::GetAll(cps); + for (ContentParent* cp : cps) { + nsresult rv = cp->TransmitPermissionsForPrincipal(aPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} diff --git a/gfx/layers/ipc/PWebRenderBridge.ipdl b/gfx/layers/ipc/PWebRenderBridge.ipdl index 9f894d613f42..766041f9f4a1 100644 --- a/gfx/layers/ipc/PWebRenderBridge.ipdl +++ b/gfx/layers/ipc/PWebRenderBridge.ipdl @@ -18,6 +18,7 @@ using struct mozilla::layers::TextureInfo from "mozilla/layers/CompositorTypes.h using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h"; using mozilla::wr::ByteBuffer from "mozilla/webrender/WebRenderTypes.h"; using mozilla::wr::ImageKey from "mozilla/webrender/WebRenderTypes.h"; +using mozilla::wr::FontKey from "mozilla/webrender/WebRenderTypes.h"; using WrBuiltDisplayListDescriptor from "mozilla/webrender/webrender_ffi.h"; using WrAuxiliaryListsDescriptor from "mozilla/webrender/webrender_ffi.h"; diff --git a/gfx/layers/ipc/WebRenderMessages.ipdlh b/gfx/layers/ipc/WebRenderMessages.ipdlh index d9131339491a..175e24ec5c82 100644 --- a/gfx/layers/ipc/WebRenderMessages.ipdlh +++ b/gfx/layers/ipc/WebRenderMessages.ipdlh @@ -9,25 +9,7 @@ include LayersSurfaces; include LayersMessages; include protocol PTexture; -using WrBorderRadius from "mozilla/webrender/webrender_ffi.h"; -using WrBorderSide from "mozilla/webrender/webrender_ffi.h"; -using WrColor from "mozilla/webrender/webrender_ffi.h"; -using WrSize from "mozilla/webrender/webrender_ffi.h"; -using WrRect from "mozilla/webrender/webrender_ffi.h"; -using WrPoint from "mozilla/webrender/webrender_ffi.h"; -using WrGradientStop from "mozilla/webrender/webrender_ffi.h"; -using WrGradientExtendMode from "mozilla/webrender/webrender_ffi.h"; -using WrGlyphArray from "mozilla/webrender/webrender_ffi.h"; -using WrMixBlendMode from "mozilla/webrender/webrender_ffi.h"; -using WrBoxShadowClipMode from "mozilla/webrender/webrender_ffi.h"; -using MaybeImageMask from "mozilla/webrender/WebRenderTypes.h"; -using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h"; -using mozilla::wr::ByteBuffer from "mozilla/webrender/WebRenderTypes.h"; -using mozilla::wr::PipelineId from "mozilla/webrender/WebRenderTypes.h"; -using mozilla::wr::ImageRendering from "mozilla/webrender/WebRenderTypes.h"; using mozilla::wr::ImageKey from "mozilla/webrender/WebRenderTypes.h"; -using mozilla::wr::FontKey from "mozilla/webrender/WebRenderTypes.h"; -using mozilla::LayerIntRegion from "Units.h"; namespace mozilla { namespace layers { diff --git a/ipc/glue/MessageChannel.cpp b/ipc/glue/MessageChannel.cpp index 7c6a48529ac3..b806342c7dce 100644 --- a/ipc/glue/MessageChannel.cpp +++ b/ipc/glue/MessageChannel.cpp @@ -632,7 +632,7 @@ MessageChannel::CanSend() const void MessageChannel::WillDestroyCurrentMessageLoop() { -#if !defined(ANDROID) +#if defined(DEBUG) && !defined(ANDROID) #if defined(MOZ_CRASHREPORTER) CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("ProtocolName"), nsDependentCString(mName)); diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index dc8dfa3b29af..3ae3cddbf46d 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -100,11 +100,15 @@ function RequireObjectCoercible(v) { /* Spec: ECMAScript Draft, 6 edition May 22, 2014, 7.1.15 */ function ToLength(v) { + // Step 1. v = ToInteger(v); - if (v <= 0) - return 0; + // Step 2. + // Use max(v, 0) here, because it's easier to optimize in Ion. + // This is correct even for -0. + v = std_Math_max(v, 0); + // Step 3. // Math.pow(2, 53) - 1 = 0x1fffffffffffff return std_Math_min(v, 0x1fffffffffffff); } diff --git a/js/src/jit-test/tests/ion/math-max-arraylength.js b/js/src/jit-test/tests/ion/math-max-arraylength.js new file mode 100644 index 000000000000..dd8e915f84fc --- /dev/null +++ b/js/src/jit-test/tests/ion/math-max-arraylength.js @@ -0,0 +1,25 @@ +var arrays = [ + [], + [1], + [1, 2], + [1, 2, 3], + [1, 2, 3, 4], +]; + +function test() { + for (var i = 0; i < arrays.length; i++) { + var array = arrays[i]; + + assertEq(Math.max(array.length, 0), i); + assertEq(Math.max(0, array.length), i); + + assertEq(Math.max(array.length, -1), i); + assertEq(Math.max(-1, array.length), i); + + assertEq(Math.max(array.length, -1.5), i); + assertEq(Math.max(-1.5, array.length), i); + } +} + +test(); +test(); diff --git a/js/src/jit-test/tests/self-hosting/tolength.js b/js/src/jit-test/tests/self-hosting/tolength.js new file mode 100644 index 000000000000..06fda18a724c --- /dev/null +++ b/js/src/jit-test/tests/self-hosting/tolength.js @@ -0,0 +1,13 @@ +let ToLength = getSelfHostedValue('ToLength'); + +assertEq(ToLength(NaN), 0); +assertEq(ToLength(-0), 0); +assertEq(ToLength(0), 0); +assertEq(ToLength(-Infinity), 0); +assertEq(ToLength(-Math.pow(2, 31)), 0); + +const MAX = Math.pow(2, 53) - 1; +assertEq(ToLength(Infinity), MAX); +assertEq(ToLength(MAX + 1), MAX); +assertEq(ToLength(3), 3); +assertEq(ToLength(40.5), 40); diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp index 721e3fb684e7..71fe1ab09f36 100644 --- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -3366,6 +3366,16 @@ MMinMax::foldsTo(TempAllocator& alloc) return toDouble; } } + + if (operand->isArrayLength() && constant->type() == MIRType::Int32) { + MOZ_ASSERT(operand->type() == MIRType::Int32); + + // max(array.length, 0) = array.length + // ArrayLength is always >= 0, so just return it. + if (isMax() && constant->toInt32() <= 0) + return operand; + } + return this; } diff --git a/layout/generic/ReflowInput.cpp b/layout/generic/ReflowInput.cpp index b8bff5bdbcd9..581f4371bd22 100644 --- a/layout/generic/ReflowInput.cpp +++ b/layout/generic/ReflowInput.cpp @@ -93,6 +93,9 @@ ReflowInput::ReflowInput(nsPresContext* aPresContext, if (aFlags & B_CLAMP_MARGIN_BOX_MIN_SIZE) { mFlags.mBClampMarginBoxMinSize = true; } + if (aFlags & I_APPLY_AUTO_MIN_SIZE) { + mFlags.mApplyAutoMinSize = true; + } if (!(aFlags & CALLER_WILL_INIT)) { Init(aPresContext); @@ -242,6 +245,7 @@ ReflowInput::ReflowInput( mFlags.mIOffsetsNeedCSSAlign = mFlags.mBOffsetsNeedCSSAlign = false; mFlags.mIClampMarginBoxMinSize = !!(aFlags & I_CLAMP_MARGIN_BOX_MIN_SIZE); mFlags.mBClampMarginBoxMinSize = !!(aFlags & B_CLAMP_MARGIN_BOX_MIN_SIZE); + mFlags.mApplyAutoMinSize = !!(aFlags & I_APPLY_AUTO_MIN_SIZE); mDiscoveredClearance = nullptr; mPercentBSizeObserver = (aParentReflowInput.mPercentBSizeObserver && @@ -1666,6 +1670,10 @@ ReflowInput::InitAbsoluteConstraints(nsPresContext* aPresContext, computeSizeFlags = ComputeSizeFlags(computeSizeFlags | ComputeSizeFlags::eBClampMarginBoxMinSize); } + if (mFlags.mApplyAutoMinSize) { + computeSizeFlags = ComputeSizeFlags(computeSizeFlags | + ComputeSizeFlags::eIApplyAutoMinSize); + } if (mFlags.mShrinkWrap) { computeSizeFlags = ComputeSizeFlags(computeSizeFlags | ComputeSizeFlags::eShrinkWrap); @@ -2379,6 +2387,10 @@ ReflowInput::InitConstraints(nsPresContext* aPresContext, computeSizeFlags = ComputeSizeFlags(computeSizeFlags | ComputeSizeFlags::eBClampMarginBoxMinSize); } + if (mFlags.mApplyAutoMinSize) { + computeSizeFlags = ComputeSizeFlags(computeSizeFlags | + ComputeSizeFlags::eIApplyAutoMinSize); + } if (mFlags.mShrinkWrap) { computeSizeFlags = ComputeSizeFlags(computeSizeFlags | ComputeSizeFlags::eShrinkWrap); diff --git a/layout/generic/ReflowInput.h b/layout/generic/ReflowInput.h index 541102a41a6a..bad643875df7 100644 --- a/layout/generic/ReflowInput.h +++ b/layout/generic/ReflowInput.h @@ -220,6 +220,7 @@ public: bool mStaticPosIsCBOrigin : 1; // the STATIC_POS_IS_CB_ORIGIN ctor flag bool mIClampMarginBoxMinSize : 1; // the I_CLAMP_MARGIN_BOX_MIN_SIZE ctor flag bool mBClampMarginBoxMinSize : 1; // the B_CLAMP_MARGIN_BOX_MIN_SIZE ctor flag + bool mApplyAutoMinSize : 1; // the I_APPLY_AUTO_MIN_SIZE ctor flag // If set, the following two flags indicate that: // (1) this frame is absolutely-positioned (or fixed-positioned). @@ -739,6 +740,9 @@ public: // Pass ComputeSizeFlags::eBClampMarginBoxMinSize to ComputeSize(). B_CLAMP_MARGIN_BOX_MIN_SIZE = (1<<6), + + // Pass ComputeSizeFlags::eIApplyAutoMinSize to ComputeSize(). + I_APPLY_AUTO_MIN_SIZE = (1<<7), }; // This method initializes various data members. It is automatically diff --git a/layout/generic/nsContainerFrame.cpp b/layout/generic/nsContainerFrame.cpp index be579c802189..54a9ebbcfdb0 100644 --- a/layout/generic/nsContainerFrame.cpp +++ b/layout/generic/nsContainerFrame.cpp @@ -866,8 +866,8 @@ nsContainerFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, void nsContainerFrame::ReflowChild(nsIFrame* aKidFrame, nsPresContext* aPresContext, - ReflowOutput& aDesiredSize, - const ReflowInput& aReflowInput, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, const WritingMode& aWM, const LogicalPoint& aPos, const nsSize& aContainerSize, @@ -880,6 +880,9 @@ nsContainerFrame::ReflowChild(nsIFrame* aKidFrame, NS_ASSERTION(aContainerSize.width != NS_UNCONSTRAINEDSIZE, "ReflowChild with unconstrained container width!"); } + MOZ_ASSERT(aDesiredSize.VisualOverflow() == nsRect(0,0,0,0) && + aDesiredSize.ScrollableOverflow() == nsRect(0,0,0,0), + "please reset the overflow areas before calling ReflowChild"); // Position the child frame and its view if requested. if (NS_FRAME_NO_MOVE_FRAME != (aFlags & NS_FRAME_NO_MOVE_FRAME)) { diff --git a/layout/generic/nsFrame.cpp b/layout/generic/nsFrame.cpp index 9d35e20c0cb1..7d4269d98eb5 100644 --- a/layout/generic/nsFrame.cpp +++ b/layout/generic/nsFrame.cpp @@ -5071,7 +5071,7 @@ nsFrame::ComputeSize(nsRenderingContext* aRenderingContext, ComputeISizeValue(aRenderingContext, aCBSize.ISize(aWM), boxSizingAdjust.ISize(aWM), boxSizingToMarginEdgeISize, minISizeCoord, aFlags); - } else if (MOZ_UNLIKELY(isGridItem)) { + } else if (MOZ_UNLIKELY(aFlags & eIApplyAutoMinSize)) { // This implements "Implied Minimum Size of Grid Items". // https://drafts.csswg.org/css-grid/#min-size-auto minISize = std::min(maxISize, GetMinISize(aRenderingContext)); diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp index 59c2f2d24dd0..9e5efeab80bd 100644 --- a/layout/generic/nsGfxScrollFrame.cpp +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -341,6 +341,7 @@ nsHTMLScrollFrame::TryLayout(ScrollReflowInput* aState, nsLayoutUtils::MarkIntrinsicISizesDirtyIfDependentOnBSize( mHelper.mScrolledFrame); } + aKidMetrics->mOverflowAreas.Clear(); ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics, false); } @@ -694,6 +695,7 @@ nsHTMLScrollFrame::ReflowContents(ScrollReflowInput* aState, insideBorderSize); if (nsRect(nsPoint(0, 0), insideBorderSize).Contains(scrolledRect)) { // Let's pretend we had no scrollbars coming in here + kidDesiredSize.mOverflowAreas.Clear(); ReflowScrolledFrame(aState, false, false, &kidDesiredSize, false); } } diff --git a/layout/generic/nsGridContainerFrame.cpp b/layout/generic/nsGridContainerFrame.cpp index fd2dddf23316..38cada4b9ebd 100644 --- a/layout/generic/nsGridContainerFrame.cpp +++ b/layout/generic/nsGridContainerFrame.cpp @@ -573,8 +573,11 @@ struct nsGridContainerFrame::GridItemInfo // Ditto *-content:[last ]baseline. Mutually exclusive w. eSelfBaseline. eContentBaseline = 0x10, eAllBaselineBits = eIsBaselineAligned | eSelfBaseline | eContentBaseline, + // Should apply Automatic Minimum Size per: + // https://drafts.csswg.org/css-grid/#min-size-auto + eApplyAutoMinSize = 0x20, // Clamp per https://drafts.csswg.org/css-grid/#min-size-auto - eClampMarginBoxMinSize = 0x20, + eClampMarginBoxMinSize = 0x40, }; explicit GridItemInfo(nsIFrame* aFrame, @@ -606,11 +609,11 @@ struct nsGridContainerFrame::GridItemInfo return aAlign; } - // Return true if we should we clamp this item's Automatic Minimum Size. + // Return true if we should apply Automatic Minimum Size to this item. // https://drafts.csswg.org/css-grid/#min-size-auto - bool ShouldClampMinSize(WritingMode aContainerWM, - LogicalAxis aContainerAxis, - nscoord aPercentageBasis) const + bool ShouldApplyAutoMinSize(WritingMode aContainerWM, + LogicalAxis aContainerAxis, + nscoord aPercentageBasis) const { const auto pos = mFrame->StylePosition(); const auto& size = aContainerAxis == eLogicalAxisInline ? @@ -3454,8 +3457,11 @@ MeasuringReflow(nsIFrame* aChild, parent->Properties().Set( nsContainerFrame::DebugReflowingWithInfiniteISize(), true); #endif - uint32_t riFlags = ReflowInput::COMPUTE_SIZE_SHRINK_WRAP | - ReflowInput::COMPUTE_SIZE_USE_AUTO_BSIZE; + auto wm = aChild->GetWritingMode(); + uint32_t riFlags = ReflowInput::COMPUTE_SIZE_USE_AUTO_BSIZE; + if (aAvailableSize.ISize(wm) == INFINITE_ISIZE_COORD) { + riFlags |= ReflowInput::COMPUTE_SIZE_SHRINK_WRAP; + } if (aIMinSizeClamp != NS_MAXSIZE) { riFlags |= ReflowInput::I_CLAMP_MARGIN_BOX_MIN_SIZE; } @@ -3480,7 +3486,6 @@ MeasuringReflow(nsIFrame* aChild, ReflowOutput childSize(childRI); nsReflowStatus childStatus; const uint32_t flags = NS_FRAME_NO_MOVE_FRAME | NS_FRAME_NO_SIZE_VIEW; - WritingMode wm = childRI.GetWritingMode(); parent->ReflowChild(aChild, pc, childSize, childRI, wm, LogicalPoint(wm), nsSize(), flags, childStatus); parent->FinishReflowChild(aChild, pc, childSize, &childRI, wm, @@ -3742,9 +3747,9 @@ nsGridContainerFrame::Tracks::ResolveIntrinsicSizeStep1( WritingMode wm = aState.mWM; // Calculate data for "Automatic Minimum Size" clamping, if needed. bool needed = ((sz.mState & TrackSize::eIntrinsicMinSizing) || - aConstraint == SizingConstraint::eNoConstraint); - if (needed && TrackSize::IsDefiniteMaxSizing(sz.mState) && - aGridItem.ShouldClampMinSize(wm, mAxis, aPercentageBasis)) { + aConstraint == SizingConstraint::eNoConstraint) && + (aGridItem.mState[mAxis] & ItemState::eApplyAutoMinSize); + if (needed && TrackSize::IsDefiniteMaxSizing(sz.mState)) { if (sz.mState & TrackSize::eIntrinsicMinSizing) { auto maxCoord = aFunctions.MaxSizingFor(aRange.mStart); cache.mMinSizeClamp = @@ -4147,6 +4152,14 @@ nsGridContainerFrame::Tracks::ResolveIntrinsicSize( iter.Reset(); for (; !iter.AtEnd(); iter.Next()) { auto& gridItem = aGridItems[iter.ItemIndex()]; + + // Check if we need to apply "Automatic Minimum Size" and cache it. + MOZ_ASSERT(!(gridItem.mState[mAxis] & ItemState::eApplyAutoMinSize), + "Why is eApplyAutoMinSize set already?"); + if (gridItem.ShouldApplyAutoMinSize(wm, mAxis, aPercentageBasis)) { + gridItem.mState[mAxis] |= ItemState::eApplyAutoMinSize; + } + const GridArea& area = gridItem.mArea; const LineRange& lineRange = area.*aRange; uint32_t span = lineRange.Extent(); @@ -4172,9 +4185,9 @@ nsGridContainerFrame::Tracks::ResolveIntrinsicSize( CachedIntrinsicSizes cache; // Calculate data for "Automatic Minimum Size" clamping, if needed. bool needed = ((state & TrackSize::eIntrinsicMinSizing) || - aConstraint == SizingConstraint::eNoConstraint); - if (needed && TrackSize::IsDefiniteMaxSizing(state) && - gridItem.ShouldClampMinSize(wm, mAxis, aPercentageBasis)) { + aConstraint == SizingConstraint::eNoConstraint) && + (gridItem.mState[mAxis] & ItemState::eApplyAutoMinSize); + if (needed && TrackSize::IsDefiniteMaxSizing(state)) { nscoord minSizeClamp = 0; for (auto i = lineRange.mStart, end = lineRange.mEnd; i < end; ++i) { auto maxCoord = aFunctions.MaxSizingFor(i); @@ -4210,11 +4223,14 @@ nsGridContainerFrame::Tracks::ResolveIntrinsicSize( gridItem.mState[mAxis] |= ItemState::eIsFlexing; } else if (aConstraint == SizingConstraint::eNoConstraint && TrackSize::IsDefiniteMaxSizing(state) && - gridItem.ShouldClampMinSize(wm, mAxis, aPercentageBasis)) { + (gridItem.mState[mAxis] & ItemState::eApplyAutoMinSize)) { gridItem.mState[mAxis] |= ItemState::eClampMarginBoxMinSize; } } } + MOZ_ASSERT(!(gridItem.mState[mAxis] & ItemState::eClampMarginBoxMinSize) || + (gridItem.mState[mAxis] & ItemState::eApplyAutoMinSize), + "clamping only applies to Automatic Minimum Size"); } // Step 2. @@ -4996,6 +5012,9 @@ nsGridContainerFrame::ReflowInFlowChild(nsIFrame* aChild, } else { aChild->Properties().Delete(BClampMarginBoxMinSizeProperty()); } + if ((aGridItemInfo->mState[childIAxis] & ItemState::eApplyAutoMinSize)) { + flags |= ReflowInput::I_APPLY_AUTO_MIN_SIZE; + } } if (!isConstrainedBSize) { diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h index afdce684fa04..8b1e31f22248 100644 --- a/layout/generic/nsIFrame.h +++ b/layout/generic/nsIFrame.h @@ -2250,6 +2250,14 @@ public: */ eIClampMarginBoxMinSize = 1 << 2, // clamp in our inline axis eBClampMarginBoxMinSize = 1 << 3, // clamp in our block axis + /** + * The frame is stretching (per CSS Box Alignment) and doesn't have an + * Automatic Minimum Size in the indicated axis. + * (may be used for both flex/grid items, but currently only used for Grid) + * https://drafts.csswg.org/css-grid/#min-size-auto + * https://drafts.csswg.org/css-align-3/#valdef-justify-self-stretch + */ + eIApplyAutoMinSize = 1 << 4, // only has an effect when eShrinkWrap is false }; /** diff --git a/layout/reftests/css-grid/bug1349571-ref.html b/layout/reftests/css-grid/bug1349571-ref.html new file mode 100644 index 000000000000..1f5a73f05bde --- /dev/null +++ b/layout/reftests/css-grid/bug1349571-ref.html @@ -0,0 +1,34 @@ + + + + + Testcase for bug 1349571 + + + + +
+
+
+ + + diff --git a/layout/reftests/css-grid/bug1349571.html b/layout/reftests/css-grid/bug1349571.html new file mode 100644 index 000000000000..e21b487b3f93 --- /dev/null +++ b/layout/reftests/css-grid/bug1349571.html @@ -0,0 +1,38 @@ + + + + + Testcase for bug 1349571 + + + + +
+
+
+
+
+ + + diff --git a/layout/reftests/css-grid/bug1350925-ref.html b/layout/reftests/css-grid/bug1350925-ref.html new file mode 100644 index 000000000000..0d8e58142706 --- /dev/null +++ b/layout/reftests/css-grid/bug1350925-ref.html @@ -0,0 +1,32 @@ + + + + + Testcase for bug 1350925 + + + + +
+
+
+ +
+
+
+ + + diff --git a/layout/reftests/css-grid/bug1350925.html b/layout/reftests/css-grid/bug1350925.html new file mode 100644 index 000000000000..a2a81b16e747 --- /dev/null +++ b/layout/reftests/css-grid/bug1350925.html @@ -0,0 +1,32 @@ + + + + + Testcase for bug 1350925 + + + + +
+
+
+ +
+
+
+ + + diff --git a/layout/reftests/css-grid/bug1356820-ref.html b/layout/reftests/css-grid/bug1356820-ref.html new file mode 100644 index 000000000000..b0cd50fea636 --- /dev/null +++ b/layout/reftests/css-grid/bug1356820-ref.html @@ -0,0 +1,25 @@ + +
+
+ first item with a longlonglongword +
+
+ second item +
+
+
+
+ first item with a longlonglongword +
+
+ second item +
+
+
+
+ first item with a longlonglongword +
+
+ second item +
+
diff --git a/layout/reftests/css-grid/bug1356820.html b/layout/reftests/css-grid/bug1356820.html new file mode 100644 index 000000000000..e37c0d049f8d --- /dev/null +++ b/layout/reftests/css-grid/bug1356820.html @@ -0,0 +1,25 @@ + +
+
+ first item with a longlonglongword +
+
+ second item +
+
+
+
+ first item with a longlonglongword +
+
+ second item +
+
+
+
+ first item with a longlonglongword +
+
+ second item +
+
diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-001-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-001-ref.html new file mode 100644 index 000000000000..463bbb4e6eef --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-001-ref.html @@ -0,0 +1,78 @@ + + + + + CSS Grid Reference: stretching overflow!=visible items + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-001.html b/layout/reftests/css-grid/grid-item-overflow-stretch-001.html new file mode 100644 index 000000000000..4f6259abeb2d --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-001.html @@ -0,0 +1,74 @@ + + + + + CSS Grid Test: stretching overflow!=visible items + + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-002-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-002-ref.html new file mode 100644 index 000000000000..a9690a54e533 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-002-ref.html @@ -0,0 +1,79 @@ + + + + + CSS Grid Reference: stretching overflow!=visible vertical-rl items + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-002.html b/layout/reftests/css-grid/grid-item-overflow-stretch-002.html new file mode 100644 index 000000000000..520eed911a9f --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-002.html @@ -0,0 +1,75 @@ + + + + + CSS Grid Test: stretching overflow!=visible vertical-rl items + + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-003-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-003-ref.html new file mode 100644 index 000000000000..c082e6be4166 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-003-ref.html @@ -0,0 +1,84 @@ + + + + + CSS Grid Reference: margin:auto stretch items + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-003.html b/layout/reftests/css-grid/grid-item-overflow-stretch-003.html new file mode 100644 index 000000000000..8bcd79d9bee9 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-003.html @@ -0,0 +1,75 @@ + + + + + CSS Grid Test: margin:auto stretch items + + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-004-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-004-ref.html new file mode 100644 index 000000000000..71ed28d7cc3d --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-004-ref.html @@ -0,0 +1,88 @@ + + + + + CSS Grid Reference: stretching items + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-004.html b/layout/reftests/css-grid/grid-item-overflow-stretch-004.html new file mode 100644 index 000000000000..b983b5184b10 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-004.html @@ -0,0 +1,82 @@ + + + + + CSS Grid Test: stretching items + + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-005-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-005-ref.html new file mode 100644 index 000000000000..e7d353c8b886 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-005-ref.html @@ -0,0 +1,83 @@ + + + + + CSS Grid Reference: stretching overflow!=visible items + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-005.html b/layout/reftests/css-grid/grid-item-overflow-stretch-005.html new file mode 100644 index 000000000000..33fe468d742a --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-005.html @@ -0,0 +1,77 @@ + + + + + CSS Grid Test: stretching overflow!=visible items + + + + + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-006-ref.html b/layout/reftests/css-grid/grid-item-overflow-stretch-006-ref.html new file mode 100644 index 000000000000..71d4d4f542f5 --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-006-ref.html @@ -0,0 +1,54 @@ + + + + + CSS Grid Reference: stretching overflow visible items + + + + + +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/grid-item-overflow-stretch-006.html b/layout/reftests/css-grid/grid-item-overflow-stretch-006.html new file mode 100644 index 000000000000..015c50fccd4f --- /dev/null +++ b/layout/reftests/css-grid/grid-item-overflow-stretch-006.html @@ -0,0 +1,56 @@ + + + + + CSS Grid Test: stretching overflow visible items + + + + + + +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ + + diff --git a/layout/reftests/css-grid/reftest.list b/layout/reftests/css-grid/reftest.list index 8c7b2bbca73b..abcdc9de0086 100644 --- a/layout/reftests/css-grid/reftest.list +++ b/layout/reftests/css-grid/reftest.list @@ -116,6 +116,12 @@ skip-if(Android) == grid-auto-min-sizing-percent-001.html grid-auto-min-sizing-p == grid-item-auto-min-size-clamp-005.html grid-item-auto-min-size-clamp-005-ref.html == grid-item-auto-min-size-clamp-006.html grid-item-auto-min-size-clamp-006-ref.html == grid-item-auto-min-size-clamp-007.html grid-item-auto-min-size-clamp-007-ref.html +== grid-item-overflow-stretch-001.html grid-item-overflow-stretch-001-ref.html +== grid-item-overflow-stretch-002.html grid-item-overflow-stretch-002-ref.html +== grid-item-overflow-stretch-003.html grid-item-overflow-stretch-003-ref.html +== grid-item-overflow-stretch-004.html grid-item-overflow-stretch-004-ref.html +== grid-item-overflow-stretch-005.html grid-item-overflow-stretch-005-ref.html +== grid-item-overflow-stretch-006.html grid-item-overflow-stretch-006-ref.html == grid-item-canvas-001.html grid-item-canvas-001-ref.html skip-if(Android) == grid-item-button-001.html grid-item-button-001-ref.html == grid-item-table-stretch-001.html grid-item-table-stretch-001-ref.html @@ -277,3 +283,6 @@ asserts(1-10) == grid-fragmentation-dyn4-021.html grid-fragmentation-021-ref.htm == grid-percent-intrinsic-sizing-001.html grid-percent-intrinsic-sizing-001-ref.html == grid-measuring-reflow-resize-static-001.html grid-measuring-reflow-resize-001-ref.html == grid-measuring-reflow-resize-dynamic-001.html grid-measuring-reflow-resize-001-ref.html +== bug1349571.html bug1349571-ref.html +== bug1356820.html bug1356820-ref.html +== bug1350925.html bug1350925-ref.html diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java index dc145cc889bc..37fe1a4c85a1 100644 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -1851,6 +1851,10 @@ public class BrowserApp extends GeckoApp case "Menu:Add": final MenuItemInfo info = new MenuItemInfo(); info.label = message.getString("name"); + if (info.label == null) { + Log.e(LOGTAG, "Invalid menu item name"); + return; + } info.id = message.getInt("id") + ADDON_MENU_OFFSET; info.checked = message.getBoolean("checked", false); info.enabled = message.getBoolean("enabled", true); diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java index fed4d14066a8..1a5c3d623193 100644 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java +++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java @@ -215,6 +215,7 @@ public class DownloadAction extends BaseAction { protected void extract(File sourceFile, File destinationFile, String checksum) throws UnrecoverableDownloadContentException, RecoverableDownloadContentException { InputStream inputStream = null; + InputStream gzInputStream = null; OutputStream outputStream = null; File temporaryFile = null; @@ -226,13 +227,15 @@ public class DownloadAction extends BaseAction { temporaryFile = new File(destinationDirectory, destinationFile.getName() + ".tmp"); - inputStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(sourceFile))); + // We have to have keep a handle to the BufferedInputStream: the GZIPInputStream + // constructor can fail e.g. if the stream isn't a GZIP stream. If we didn't keep + // a reference to that stream we wouldn't be able to close it if GZInputStream throws. + // (The BufferedInputStream constructor doesn't throw, so we don't need to care about it.) + inputStream = new BufferedInputStream(new FileInputStream(sourceFile)); + gzInputStream = new GZIPInputStream(inputStream); outputStream = new BufferedOutputStream(new FileOutputStream(temporaryFile)); - IOUtils.copy(inputStream, outputStream); - - inputStream.close(); - outputStream.close(); + IOUtils.copy(gzInputStream, outputStream); if (!verify(temporaryFile, checksum)) { Log.w(LOGTAG, "Checksum of extracted file does not match."); @@ -244,6 +247,7 @@ public class DownloadAction extends BaseAction { // We could not extract to the destination: Keep temporary file and try again next time we run. throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e); } finally { + IOUtils.safeStreamClose(gzInputStream); IOUtils.safeStreamClose(inputStream); IOUtils.safeStreamClose(outputStream); diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 75c01e083142..974c63cf9af2 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -369,7 +369,6 @@ gvjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x 'GeckoSharedPrefs.java', 'GeckoThread.java', 'GeckoView.java', - 'GeckoViewChrome.java', 'GeckoViewFragment.java', 'GeckoViewSettings.java', 'gfx/BitmapUtils.java', diff --git a/mobile/android/components/geckoview/GeckoView.manifest b/mobile/android/components/geckoview/GeckoView.manifest new file mode 100644 index 000000000000..f1268ba7b725 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoView.manifest @@ -0,0 +1,10 @@ +# GeckoViewPrompt.js +component {076ac188-23c1-4390-aa08-7ef1f78ca5d9} GeckoViewPrompt.js +contract @mozilla.org/embedcomp/prompt-service;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9} +contract @mozilla.org/prompter;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9} +category app-startup GeckoViewPrompt service,@mozilla.org/prompter;1 +category profile-after-change GeckoViewPrompt @mozilla.org/prompter;1 process=main +component {aa0dd6fc-73dd-4621-8385-c0b377e02cee} GeckoViewPrompt.js process=main +contract @mozilla.org/colorpicker;1 {aa0dd6fc-73dd-4621-8385-c0b377e02cee} process=main +component {e4565e36-f101-4bf5-950b-4be0887785a9} GeckoViewPrompt.js process=main +contract @mozilla.org/filepicker;1 {e4565e36-f101-4bf5-950b-4be0887785a9} process=main diff --git a/mobile/android/components/geckoview/GeckoViewPrompt.js b/mobile/android/components/geckoview/GeckoViewPrompt.js new file mode 100644 index 000000000000..c6e72d993e05 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompt.js @@ -0,0 +1,1062 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ContentPrefServiceParent", + "resource://gre/modules/ContentPrefServiceParent.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", + "resource://gre/modules/Messaging.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "UUIDGen", + "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); + +function PromptFactory() { +} + +PromptFactory.prototype = { + classID: Components.ID("{076ac188-23c1-4390-aa08-7ef1f78ca5d9}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]), + + /* ---------- nsIObserver ---------- */ + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": { + Services.obs.addObserver(this, "chrome-document-global-created"); + Services.obs.addObserver(this, "content-document-global-created"); + break; + } + case "profile-after-change": { + // ContentPrefServiceParent is needed for e10s file picker. + ContentPrefServiceParent.init(); + Services.mm.addMessageListener("GeckoView:Prompt", this); + break; + } + case "chrome-document-global-created": + case "content-document-global-created": { + let win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell).QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (win !== aSubject) { + // Only attach to top-level windows. + return; + } + win.addEventListener("click", this); // non-capture + win.addEventListener("contextmenu", this); // non-capture + break; + } + } + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "click": + this._handleClick(aEvent); + break; + case "contextmenu": + this._handleContextMenu(aEvent); + break; + } + }, + + _handleClick: function(aEvent) { + let target = aEvent.target; + if (aEvent.defaultPrevented || target.isContentEditable || + target.disabled || target.readOnly || !target.willValidate) { + // target.willValidate is false when any associated fieldset is disabled, + // in which case this element is treated as disabled per spec. + return; + } + + let win = target.ownerGlobal; + if (target instanceof win.HTMLSelectElement) { + this._handleSelect(target); + aEvent.preventDefault(); + } else if (target instanceof win.HTMLInputElement) { + let type = target.type; + if (type === "date" || type === "month" || type === "week" || + type === "time" || type === "datetime-local") { + this._handleDateTime(target, type); + aEvent.preventDefault(); + } + } + }, + + _handleSelect: function(aElement) { + let win = aElement.ownerGlobal; + let id = 0; + let map = {}; + + let items = (function enumList(elem, disabled) { + let items = []; + let children = elem.children; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + if (win.getComputedStyle(child).display === "none") { + continue; + } + let item = { + id: String(id), + disabled: disabled || child.disabled, + }; + if (child instanceof win.HTMLOptGroupElement) { + item.label = child.label; + item.items = enumList(child, item.disabled); + } else if (child instanceof win.HTMLOptionElement) { + item.label = child.label || child.text; + item.selected = child.selected; + } else { + continue; + } + items.push(item); + map[id++] = child; + } + return items; + })(aElement); + + let prompt = new PromptDelegate(win); + prompt.asyncShowPrompt({ + type: "choice", + mode: aElement.multiple ? "multiple" : "single", + choices: items, + }, result => { + // OK: result + // Cancel: !result + if (!result || result.choices === undefined) { + return; + } + + let dispatchEvents = false; + if (!aElement.multiple) { + let elem = map[result.choices[0]]; + if (elem) { + dispatchEvents = !elem.selected; + elem.selected = true; + } else { + Cu.reportError("Invalid id for select result: " + result.choices[0]); + } + } else { + for (let i = 0; i < id; i++) { + let elem = map[i]; + let index = result.choices.indexOf(String(i)); + if (elem.selected != (index >= 0)) { + // Current selected is not the same as the new selected state. + dispatchEvents = true; + elem.selected = !elem.selected; + result.choices[index] = undefined; + } + } + for (let i = 0; i < result.choices.length; i++) { + if (result.choices[i] !== undefined && result.choices[i] !== null) { + Cu.reportError("Invalid id for select result: " + result.choices[0]); + break; + } + } + } + + if (dispatchEvents) { + this._dispatchEvents(aElement); + } + }); + }, + + _handleDateTime: function(aElement, aType) { + let prompt = new PromptDelegate(aElement.ownerGlobal); + prompt.asyncShowPrompt({ + type: "datetime", + mode: aType, + value: aElement.value, + min: aElement.min, + max: aElement.max, + }, result => { + // OK: result + // Cancel: !result + if (!result || result.datetime === undefined || result.datetime === aElement.value) { + return; + } + aElement.value = result.datetime; + this._dispatchEvents(aElement); + }); + }, + + _dispatchEvents: function(aElement) { + // Fire both "input" and "change" events for . + aElement.dispatchEvent(new aElement.ownerGlobal.Event("input", { bubbles: true })); + aElement.dispatchEvent(new aElement.ownerGlobal.Event("change", { bubbles: true })); + }, + + _handleContextMenu: function(aEvent) { + let target = aEvent.target; + if (aEvent.defaultPrevented || target.isContentEditable) { + return; + } + + // Look through all ancestors for a context menu per spec. + let parent = target; + let menu = target.contextMenu; + while (!menu && parent) { + menu = parent.contextMenu; + parent = parent.parentElement; + } + if (!menu) { + return; + } + + let builder = { + _cursor: undefined, + _id: 0, + _map: {}, + _stack: [], + items: [], + + // nsIMenuBuilder + openContainer: function(aLabel) { + if (!this._cursor) { + // Top-level + this._cursor = this; + return; + } + let newCursor = { + id: String(this._id++), + items: [], + label: aLabel, + }; + this._cursor.items.push(newCursor); + this._stack.push(this._cursor); + this._cursor = newCursor; + }, + + addItemFor: function(aElement, aCanLoadIcon) { + this._cursor.items.push({ + disabled: aElement.disabled, + icon: aCanLoadIcon && aElement.icon && + aElement.icon.length ? aElement.icon : null, + id: String(this._id), + label: aElement.label, + selected: aElement.checked, + }); + this._map[this._id++] = aElement; + }, + + addSeparator: function() { + this._cursor.items.push({ + disabled: true, + id: String(this._id++), + separator: true, + }); + }, + + undoAddSeparator: function() { + let sep = this._cursor.items[this._cursor.items.length - 1]; + if (sep && sep.separator) { + this._cursor.items.pop(); + } + }, + + closeContainer: function() { + let childItems = (this._cursor.label === "") ? this._cursor.items : null; + this._cursor = this._stack.pop(); + + if (childItems !== null && this._cursor && this._cursor.items.length === 1) { + // Merge a single nameless child container into the parent container. + // This lets us build an HTML contextmenu within a submenu. + this._cursor.items = childItems; + } + }, + + toJSONString: function() { + return JSON.stringify(this.items); + }, + + click: function(aId) { + let item = this._map[aId]; + if (item) { + item.click(); + } + }, + }; + + // XXX the "show" event is not cancelable but spec says it should be. + menu.sendShowEvent(); + menu.build(builder); + + let prompt = new PromptDelegate(target.ownerGlobal); + prompt.asyncShowPrompt({ + type: "choice", + mode: "menu", + choices: builder.items, + }, result => { + // OK: result + // Cancel: !result + if (result && result.choices !== undefined) { + builder.click(result.choices[0]); + } + }); + + aEvent.preventDefault(); + }, + + receiveMessage: function(aMsg) { + if (aMsg.name !== "GeckoView:Prompt") { + return; + } + + let prompt = new PromptDelegate(aMsg.target.contentWindow || aMsg.target.ownerGlobal); + prompt.asyncShowPrompt(aMsg.data, result => { + aMsg.target.messageManager.sendAsyncMessage("GeckoView:PromptClose", { + uuid: aMsg.data.uuid, + result: result, + }); + }); + }, + + /* ---------- nsIPromptFactory ---------- */ + getPrompt: function(aDOMWin, aIID) { + // Delegated to login manager here, which in turn calls back into us via nsIPromptService2. + if (aIID.equals(Ci.nsIAuthPrompt2) || aIID.equals(Ci.nsIAuthPrompt)) { + try { + let pwmgr = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].getService(Ci.nsIPromptFactory); + return pwmgr.getPrompt(aDOMWin, aIID); + } catch (e) { + Cu.reportError("Delegation to password manager failed: " + e); + } + } + + let p = new PromptDelegate(aDOMWin); + p.QueryInterface(aIID); + return p; + }, + + /* ---------- private memebers ---------- */ + + // nsIPromptService and nsIPromptService2 methods proxy to our Prompt class + callProxy: function(aMethod, aArguments) { + let prompt = new PromptDelegate(aArguments[0]); + return prompt[aMethod].apply(prompt, Array.prototype.slice.call(aArguments, 1)); + }, + + /* ---------- nsIPromptService ---------- */ + + alert: function() { + return this.callProxy("alert", arguments); + }, + alertCheck: function() { + return this.callProxy("alertCheck", arguments); + }, + confirm: function() { + return this.callProxy("confirm", arguments); + }, + confirmCheck: function() { + return this.callProxy("confirmCheck", arguments); + }, + confirmEx: function() { + return this.callProxy("confirmEx", arguments); + }, + prompt: function() { + return this.callProxy("prompt", arguments); + }, + promptUsernameAndPassword: function() { + return this.callProxy("promptUsernameAndPassword", arguments); + }, + promptPassword: function() { + return this.callProxy("promptPassword", arguments); + }, + select: function() { + return this.callProxy("select", arguments); + }, + + /* ---------- nsIPromptService2 ---------- */ + promptAuth: function() { + return this.callProxy("promptAuth", arguments); + }, + asyncPromptAuth: function() { + return this.callProxy("asyncPromptAuth", arguments); + } +}; + +function PromptDelegate(aDomWin) { + this._domWin = aDomWin; + + if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { + return; + } + + this._dispatcher = EventDispatcher.instance; + + if (!aDomWin) { + return; + } + let gvWin = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell).QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + try { + this._dispatcher = EventDispatcher.for(gvWin); + } catch (e) { + // Use global dispatcher. + } +} + +PromptDelegate.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + + BUTTON_TYPE_POSITIVE: 0, + BUTTON_TYPE_NEUTRAL: 1, + BUTTON_TYPE_NEGATIVE: 2, + + /* ---------- internal methods ---------- */ + + _changeModalState: function(aEntering) { + if (!this._domWin) { + // Allow not having a DOM window. + return true; + } + // Accessing the document object can throw if this window no longer exists. See bug 789888. + try { + let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (!aEntering) { + winUtils.leaveModalState(); + } + + let event = this._domWin.document.createEvent("Events"); + event.initEvent(aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed", + true, true); + winUtils.dispatchEventToChromeOnly(this._domWin, event); + + if (aEntering) { + winUtils.enterModalState(); + } + return true; + + } catch(ex) { + Cu.reportError("Failed to change modal state: " + e); + } + return false; + }, + + /** + * Shows a native prompt, and then spins the event loop for this thread while we wait + * for a response + */ + _showPrompt: function(aMsg) { + let result = undefined; + if (!this._changeModalState(/* aEntering */ true)) { + return; + } + try { + this.asyncShowPrompt(aMsg, res => result = res); + + // Spin this thread while we wait for a result + let thread = Services.tm.currentThread; + while (result === undefined) { + thread.processNextEvent(true); + } + } finally { + this._changeModalState(/* aEntering */ false); + } + return result; + }, + + asyncShowPrompt: function(aMsg, aCallback) { + if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { + let docShell = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem; + let messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild) + .messageManager; + + let uuid = UUIDGen.generateUUID().toString(); + aMsg.uuid = uuid; + + messageManager.addMessageListener("GeckoView:PromptClose", function listener(msg) { + if (msg.data.uuid !== uuid) { + return; + } + messageManager.removeMessageListener(msg.name, listener); + aCallback(msg.data.result); + }); + messageManager.sendAsyncMessage("GeckoView:Prompt", aMsg); + return; + } + + let handled = false; + this._dispatcher.dispatch("GeckoView:Prompt", aMsg, { + onSuccess: response => { + if (handled) { + return; + } + aCallback(response); + // This callback object is tied to the Java garbage collector because + // it is invoked from Java. Manually release the target callback + // here; otherwise we may hold onto resources for too long, because + // we would be relying on both the Java and the JS garbage collectors + // to run. + aMsg = undefined; + aCallback = undefined; + handled = true; + }, + onError: error => { + Cu.reportError("Prompt error: " + error); + this.onSuccess(null); + }, + }); + }, + + _addText: function(aTitle, aText, aMsg) { + return Object.assign(aMsg, { + title: aTitle, + msg: aText, + }); + }, + + _addCheck: function(aCheckMsg, aCheckState, aMsg) { + return Object.assign(aMsg, { + hasCheck: !!aCheckMsg, + checkMsg: aCheckMsg, + checkValue: aCheckState && aCheckState.value, + }); + }, + + /* ---------- nsIPrompt ---------- */ + + alert: function(aTitle, aText) { + this.alertCheck(aTitle, aText); + }, + + alertCheck: function(aTitle, aText, aCheckMsg, aCheckState) { + let result = this._showPrompt(this._addText(aTitle, aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "alert", + }))); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + }, + + confirm: function(aTitle, aText) { + // Button 0 is OK. + return this.confirmCheck(aTitle, aText) == 0; + }, + + confirmCheck: function(aTitle, aText, aCheckMsg, aCheckState) { + // Button 0 is OK. + return this.confirmEx(aTitle, aText, Ci.nsIPrompt.STD_OK_CANCEL_BUTTONS, + /* aButton0 */ null, /* aButton1 */ null, /* aButton2 */ null, + aCheckMsg, aCheckState) == 0; + }, + + confirmEx: function(aTitle, aText, aButtonFlags, aButton0, + aButton1, aButton2, aCheckMsg, aCheckState) { + let btnMap = Array(3).fill(null); + let btnTitle = Array(3).fill(null); + let btnCustomTitle = Array(3).fill(null); + let savedButtonId = []; + for (let i = 0; i < 3; i++) { + let btnFlags = aButtonFlags >> (i * 8); + switch (btnFlags & 0xff) { + case Ci.nsIPrompt.BUTTON_TITLE_OK : + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "ok"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL : + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "cancel"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES : + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "yes"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO : + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "no"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING : + // We don't know if this is positive/negative/neutral, so save for later. + savedButtonId.push(i); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE : + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE : + case Ci.nsIPrompt.BUTTON_TITLE_REVERT : + // Not supported; fall-through. + default: + break; + } + } + + // Put saved buttons into available slots. + for (let i = 0; i < 3 && savedButtonId.length; i++) { + if (btnMap[i] === null) { + btnMap[i] = savedButtonId.shift(); + btnTitle[i] = "custom"; + btnCustomTitle[i] = [aButton0, aButton1, aButton2][btnMap[i]]; + } + } + + let result = this._showPrompt(this._addText(aTitle, aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "button", + btnTitle: btnTitle, + btnCustomTitle: btnCustomTitle, + }))); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + return (result && result.button in btnMap) ? btnMap[result.button] : -1; + }, + + prompt: function(aTitle, aText, aValue, aCheckMsg, aCheckState) { + let result = this._showPrompt(this._addText(aTitle, aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "text", + value: aValue.value, + }))); + // OK: result && result.text !== undefined + // Cancel: result && result.text === undefined + // Error: !result + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + if (!result || result.text === undefined) { + return false; + } + aValue.value = result.text || ""; + return true; + }, + + promptPassword: function(aTitle, aText, aPassword, aCheckMsg, aCheckState) { + return this._promptUsernameAndPassword(aTitle, aText, /* aUsername */ undefined, + aPassword, aCheckMsg, aCheckState); + }, + + promptUsernameAndPassword: function(aTitle, aText, aUsername, aPassword, + aCheckMsg, aCheckState) { + let msg = { + type: "auth", + mode: aUsername ? "auth" : "password", + options: { + flags: aUsername ? 0 : Ci.nsIAuthInformation.ONLY_PASSWORD, + username: aUsername ? aUsername.value : undefined, + password: aPassword.value, + }, + }; + let result = this._showPrompt(this._addText(aTitle, aText, + this._addCheck(aCheckMsg, aCheckState, msg))); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + if (!result || result.password === undefined) { + return false; + } + if (aUsername) { + aUsername.value = result.username || ""; + } + aPassword.value = result.password || ""; + return true; + }, + + select: function(aTitle, aText, aCount, aSelectList, aOutSelection) { + let choices = Array.prototype.map.call(aSelectList, (item, index) => ({ + id: String(index), + label: item, + disabled: false, + selected: false, + })); + let result = this._showPrompt(this._addText(aTitle, aText, { + type: "choice", + mode: "single", + choices: choices, + })); + // OK: result + // Cancel: !result + if (!result || result.choices === undefined) { + return false; + } + aOutSelection.value = Number(result.choices[0]); + return true; + }, + + _getAuthMsg: function(aChannel, aLevel, aAuthInfo) { + let username; + if ((aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) && aAuthInfo.domain) { + username = aAuthInfo.domain + "\\" + aAuthInfo.username; + } else { + username = aAuthInfo.username; + } + return this._addText(/* title */ null, this._getAuthText(aChannel, aAuthInfo), { + type: "auth", + mode: aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD ? "password" : "auth", + options: { + flags: aAuthInfo.flags, + uri: aChannel && aChannel.URI.spec, + level: aLevel, + username: username, + password: aAuthInfo.password, + }, + }); + }, + + _fillAuthInfo: function(aAuthInfo, aCheckState, aResult) { + if (aResult && aCheckState) { + aCheckState.value = !!aResult.checkValue; + } + if (!aResult || aResult.password === undefined) { + return false; + } + + aAuthInfo.password = aResult.password || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) { + return true; + } + + let username = aResult.username || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + var idx = username.indexOf("\\"); + if (idx >= 0) { + aAuthInfo.domain = username.substring(0, idx); + aAuthInfo.username = username.substring(idx + 1); + return true; + } + } + aAuthInfo.username = username; + return true; + }, + + promptAuth: function(aChannel, aLevel, aAuthInfo, aCheckMsg, aCheckState) { + let result = this._showPrompt(this._addCheck(aCheckMsg, aCheckState, + this._getAuthMsg(aChannel, aLevel, aAuthInfo))); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + return this._fillAuthInfo(aAuthInfo, aCheckState, result); + }, + + asyncPromptAuth: function(aChannel, aCallback, aContext, aLevel, aAuthInfo, + aCheckLabel, aCheckState) { + let responded = false; + let callback = result => { + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + if (responded) { + return; + } + responded = true; + if (this._fillAuthInfo(aAuthInfo, aCheckState, result)) { + aCallback.onAuthAvailable(aContext, aAuthInfo); + } else { + aCallback.onAuthCancelled(aContext, /* userCancel */ true); + } + }; + this.asyncShowPrompt(this._addCheck(aCheckMsg, aCheckState, + this._getAuthMsg(aChannel, aLevel, aAuthInfo)), callback); + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), + cancel: function() { + if (responded) { + return; + } + responded = true; + aCallback.onAuthCancelled(aContext, /* userCancel */ false); + } + }; + }, + + _getAuthText: function(aChannel, aAuthInfo) { + let isProxy = (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY); + let isPassOnly = (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD); + let isCrossOrig = (aAuthInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE); + + let username = aAuthInfo.username; + let [displayHost, realm] = this._getAuthTarget(aChannel, aAuthInfo); + + // Suppress "the site says: $realm" when we synthesized a missing realm. + if (!aAuthInfo.realm && !isProxy) { + realm = ""; + } + + // Trim obnoxiously long realms. + if (realm.length > 50) { + realm = realm.substring(0, 50) + "\u2026"; + } + + let bundle = Services.strings.createBundle( + "chrome://global/locale/commonDialogs.properties"); + let text; + if (isProxy) { + text = bundle.formatStringFromName("EnterLoginForProxy3", [realm, displayHost], 2); + } else if (isPassOnly) { + text = bundle.formatStringFromName("EnterPasswordFor", [username, displayHost], 2); + } else if (isCrossOrig) { + text = bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [displayHost], 1); + } else if (!realm) { + text = bundle.formatStringFromName("EnterUserPasswordFor2", [displayHost], 1); + } else { + text = bundle.formatStringFromName("EnterLoginForRealm3", [realm, displayHost], 2); + } + + return text; + }, + + _getAuthTarget: function(aChannel, aAuthInfo) { + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(aChannel instanceof Ci.nsIProxiedChannel)) { + throw "proxy auth needs nsIProxiedChannel"; + } + let info = aChannel.proxyInfo; + if (!info) { + throw "proxy auth needs nsIProxyInfo"; + } + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); + let hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port; + let realm = aAuthInfo.realm; + if (!realm) { + realm = hostname; + } + return [hostname, realm]; + } + + let hostname = aChannel.URI.scheme + "://" + aChannel.URI.hostPort; + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + let realm = aAuthInfo.realm; + if (!realm) { + realm = hostname; + } + return [hostname, realm]; + }, +}; + +function FilePickerDelegate() { +} + +FilePickerDelegate.prototype = { + classID: Components.ID("{e4565e36-f101-4bf5-950b-4be0887785a9}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker]), + + /* ---------- nsIFilePicker ---------- */ + init: function(aParent, aTitle, aMode) { + if (aMode === Ci.nsIFilePicker.modeGetFolder || + aMode === Ci.nsIFilePicker.modeSave) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + this._prompt = new PromptDelegate(aParent); + this._msg = { + type: "file", + title: aTitle, + mode: (aMode === Ci.nsIFilePicker.modeOpenMultiple) ? "multiple" : "single", + }; + this._mode = aMode; + this._mimeTypes = []; + this._extensions = []; + }, + + get mode() { + return this._mode; + }, + + appendFilters: function(aFilterMask) { + if (aFilterMask & Ci.nsIFilePicker.filterAll) { + this._mimeTypes.push("*/*"); + } + if (aFilterMask & Ci.nsIFilePicker.filterAudio) { + this._mimeTypes.push("audio/*"); + } + if (aFilterMask & Ci.nsIFilePicker.filterImages) { + this._mimeTypes.push("image/*"); + } + if (aFilterMask & Ci.nsIFilePicker.filterVideo) { + this._mimeTypes.push("video/*"); + } + if (aFilterMask & Ci.nsIFilePicker.filterHTML) { + this._mimeTypes.push("text/html"); + } + if (aFilterMask & Ci.nsIFilePicker.filterText) { + this._mimeTypes.push("text/plain"); + } + if (aFilterMask & Ci.nsIFilePicker.filterXML) { + this._mimeTypes.push("text/xml"); + } + if (aFilterMask & Ci.nsIFilePicker.filterXUL) { + this._mimeTypes.push("application/vnd.mozilla.xul+xml"); + } + }, + + appendFilter: function(aTitle, aFilter) { + // Only include filter that specify extensions (i.e. exclude generic ones like "*"). + let filters = aFilter.split(/[\s,;]+/).filter(filter => filter.indexOf(".") >= 0); + Array.prototype.push.apply(this._extensions, filters); + }, + + show: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + open: function(aFilePickerShownCallback) { + this._msg.mimeTypes = this._mimeTypes; + if (this._extensions.length) { + this._msg.extensions = this._extensions; + } + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + if (!result || !result.files || !result.files.length) { + aFilePickerShownCallback.done(Ci.nsIFilePicker.returnCancel); + } else { + this._files = result.files; + aFilePickerShownCallback.done(Ci.nsIFilePicker.returnOK); + } + }); + }, + + get file() { + if (!this._files) { + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + return new FileUtils.File(this._files[0]); + }, + + get fileURL() { + return Services.io.newFileURI(this.file); + }, + + _getEnumerator(aDOMFile) { + if (!this._files) { + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]), + _owner: this, + _index: 0, + hasMoreElements: function() { + return this._index < this._owner._files.length; + }, + getNext: function() { + let files = this._owner._files; + if (this._index >= files.length) { + throw Cr.NS_ERROR_FAILURE; + } + if (aDOMFile) { + return this._owner._getDOMFile(files[this._index++]); + } + return new FileUtils.File(files[this._index++]); + } + }; + }, + + get files() { + return this._getEnumerator(/* aDOMFile */ false); + }, + + _getDOMFile(aPath) { + if (this._prompt._domWin) { + return this._prompt._domWin.File.createFromFileName(aPath); + } + return File.createFromFileName(aPath); + }, + + get domFileOrDirectory() { + if (!this._files) { + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + return this._getDOMFile(this._files[0]); + }, + + get domFileOrDirectoryEnumerator() { + return this._getEnumerator(/* aDOMFile */ true); + }, + + get defaultString() { + return ""; + }, + + set defaultString(aValue) { + }, + + get defaultExtension() { + return ""; + }, + + set defaultExtension(aValue) { + }, + + get filterIndex() { + return 0; + }, + + set filterIndex(aValue) { + }, + + get displayDirectory() { + return null; + }, + + set displayDirectory(aValue) { + }, + + get addToRecentDocs() { + return false; + }, + + set addToRecentDocs(aValue) { + }, + + get okButtonLabel() { + return ""; + }, + + set okButtonLabel(aValue) { + }, +}; + +function ColorPickerDelegate() { +} + +ColorPickerDelegate.prototype = { + classID: Components.ID("{aa0dd6fc-73dd-4621-8385-c0b377e02cee}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker]), + + init: function(aParent, aTitle, aInitialColor) { + this._prompt = new PromptDelegate(aParent); + this._msg = { + type: "color", + title: aTitle, + value: aInitialColor, + }; + }, + + open: function(aColorPickerShownCallback) { + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + aColorPickerShownCallback.done((result && result.color) || ""); + }); + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ + ColorPickerDelegate, FilePickerDelegate, PromptFactory]); diff --git a/mobile/android/components/geckoview/moz.build b/mobile/android/components/geckoview/moz.build new file mode 100644 index 000000000000..316b3e99b034 --- /dev/null +++ b/mobile/android/components/geckoview/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_COMPONENTS += [ + 'GeckoView.manifest', + 'GeckoViewPrompt.js', +] diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build index 89dd676797d6..b5fadf087dd4 100644 --- a/mobile/android/components/moz.build +++ b/mobile/android/components/moz.build @@ -50,4 +50,5 @@ EXTRA_PP_COMPONENTS += [ DIRS += [ 'extensions', 'build', + 'geckoview', ] diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java index b875fa983356..4ed291e83699 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java @@ -6,6 +6,10 @@ package org.mozilla.gecko; +import java.io.File; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Set; import org.mozilla.gecko.annotation.ReflectionTarget; @@ -17,8 +21,11 @@ import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.GeckoBundle; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -64,16 +71,30 @@ public class GeckoView extends LayerView } } + static { + EventDispatcher.getInstance().registerUiThreadListener(new BundleEventListener() { + @Override + public void handleMessage(final String event, final GeckoBundle message, + final EventCallback callback) { + if ("GeckoView:Prompt".equals(event)) { + handlePromptEvent(/* view */ null, message, callback); + } + } + }, "GeckoView:Prompt"); + } + + private static PromptDelegate sDefaultPromptDelegate; + private final NativeQueue mNativeQueue = new NativeQueue(State.INITIAL, State.READY); private final EventDispatcher mEventDispatcher = new EventDispatcher(mNativeQueue); - private ChromeDelegate mChromeDelegate; /* package */ ContentListener mContentListener; /* package */ NavigationListener mNavigationListener; /* package */ ProgressListener mProgressListener; + private PromptDelegate mPromptDelegate; private InputConnectionListener mInputConnectionListener; private GeckoViewSettings mSettings; @@ -173,6 +194,7 @@ public class GeckoView extends LayerView "GeckoView:LocationChange", "GeckoView:PageStart", "GeckoView:PageStop", + "GeckoView:Prompt", "GeckoView:SecurityChanged", null); } @@ -205,6 +227,8 @@ public class GeckoView extends LayerView if (mProgressListener != null) { mProgressListener.onPageStop(GeckoView.this, message.getBoolean("success")); } + } else if ("GeckoView:Prompt".equals(event)) { + handlePromptEvent(GeckoView.this, message, callback); } else if ("GeckoView:SecurityChanged".equals(event)) { if (mProgressListener != null) { mProgressListener.onSecurityChange(GeckoView.this, message.getInt("status")); @@ -504,15 +528,6 @@ public class GeckoView extends LayerView throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location."); } - /** - * Set the chrome callback handler. - * This will replace the current handler. - * @param chrome An implementation of GeckoViewChrome. - */ - public void setChromeDelegate(ChromeDelegate chrome) { - mChromeDelegate = chrome; - } - /** * Set the content callback handler. * This will replace the current handler. @@ -573,6 +588,398 @@ public class GeckoView extends LayerView return mNavigationListener; } + /** + * Set the default prompt delegate for all GeckoView instances. The default prompt + * delegate is used for certain types of prompts and for GeckoViews that do not have + * custom prompt delegates. + * @param delegate PromptDelegate instance or null to use the built-in delegate. + * @see #setPromptDelegate(PromptDelegate) + */ + public static void setDefaultPromptDelegate(PromptDelegate delegate) { + sDefaultPromptDelegate = delegate; + } + + /** + * Get the default prompt delegate for all GeckoView instances. + * @return PromptDelegate instance + * @see #getPromptDelegate() + */ + public static PromptDelegate getDefaultPromptDelegate() { + return sDefaultPromptDelegate; + } + + /** + * Set the current prompt delegate for this GeckoView. + * @param delegate PromptDelegate instance or null to use the default delegate. + * @see #setDefaultPromptDelegate(PromptDelegate) + */ + public void setPromptDelegate(PromptDelegate delegate) { + mPromptDelegate = delegate; + } + + /** + * Get the current prompt delegate for this GeckoView. + * @return PromptDelegate instance or null if using default delegate. + * @see #getDefaultPromptDelegate() + */ + public PromptDelegate getPromptDelegate() { + return mPromptDelegate; + } + + private static class PromptCallback implements + PromptDelegate.AlertCallback, PromptDelegate.ButtonCallback, + PromptDelegate.TextCallback, PromptDelegate.AuthCallback, + PromptDelegate.ChoiceCallback, PromptDelegate.FileCallback { + + private final String mType; + private final String mMode; + private final boolean mHasCheckbox; + private final String mCheckboxMessage; + + private EventCallback mCallback; + private boolean mCheckboxValue; + private GeckoBundle mResult; + + public PromptCallback(final String type, final String mode, + final GeckoBundle message, final EventCallback callback) { + mType = type; + mMode = mode; + mCallback = callback; + mHasCheckbox = message.getBoolean("hasCheck"); + mCheckboxMessage = message.getString("checkMsg"); + mCheckboxValue = message.getBoolean("checkValue"); + } + + private GeckoBundle ensureResult() { + if (mResult == null) { + // Usually result object contains two items. + mResult = new GeckoBundle(2); + } + return mResult; + } + + private void submit() { + if (mHasCheckbox) { + ensureResult().putBoolean("checkValue", mCheckboxValue); + } + if (mCallback != null) { + mCallback.sendSuccess(mResult); + mCallback = null; + } + } + + @Override // AlertCallbcak + public void dismiss() { + // Send a null result. + mResult = null; + submit(); + } + + @Override // AlertCallbcak + public boolean hasCheckbox() { + return mHasCheckbox; + } + + @Override // AlertCallbcak + public String getCheckboxMessage() { + return mCheckboxMessage; + } + + @Override // AlertCallbcak + public boolean getCheckboxValue() { + return mCheckboxValue; + } + + @Override // AlertCallbcak + public void setCheckboxValue(final boolean value) { + mCheckboxValue = value; + } + + @Override // ButtonCallback + public void confirm(final int value) { + if ("button".equals(mType)) { + ensureResult().putInt("button", value); + } else { + throw new UnsupportedOperationException(); + } + submit(); + } + + @Override // TextCallback, AuthCallback, ChoiceCallback, FileCallback + public void confirm(final String value) { + if ("text".equals(mType) || "color".equals(mType) || "datetime".equals(mType)) { + ensureResult().putString(mType, value); + } else if ("auth".equals(mType)) { + if (!"password".equals(mMode)) { + throw new IllegalArgumentException(); + } + ensureResult().putString("password", value); + } else if ("choice".equals(mType)) { + confirm(new String[] { value }); + return; + } else { + throw new UnsupportedOperationException(); + } + submit(); + } + + @Override // AuthCallback + public void confirm(final String username, final String password) { + if ("auth".equals(mType)) { + if (!"auth".equals(mMode)) { + throw new IllegalArgumentException(); + } + ensureResult().putString("username", username); + ensureResult().putString("password", password); + } else { + throw new UnsupportedOperationException(); + } + submit(); + } + + @Override // ChoiceCallback, FileCallback + public void confirm(final String[] values) { + if (("menu".equals(mMode) || "single".equals(mMode)) && + (values == null || values.length != 1)) { + throw new IllegalArgumentException(); + } + if ("choice".equals(mType)) { + ensureResult().putStringArray("choices", values); + } else { + throw new UnsupportedOperationException(); + } + submit(); + } + + @Override // ChoiceCallback + public void confirm(GeckoBundle item) { + if ("choice".equals(mType)) { + confirm(item == null ? null : item.getString("id")); + return; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override // ChoiceCallback + public void confirm(GeckoBundle[] items) { + if (("menu".equals(mMode) || "single".equals(mMode)) && + (items == null || items.length != 1)) { + throw new IllegalArgumentException(); + } + if ("choice".equals(mType)) { + if (items == null) { + confirm((String[]) null); + return; + } + final String[] ids = new String[items.length]; + for (int i = 0; i < ids.length; i++) { + ids[i] = (items[i] == null) ? null : items[i].getString("id"); + } + confirm(ids); + return; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override // FileCallback + public void confirm(final Uri uri) { + if ("file".equals(mType)) { + confirm(uri == null ? null : new Uri[] { uri }); + return; + } else { + throw new UnsupportedOperationException(); + } + } + + private static String getFile(final Uri uri) { + if (uri == null) { + return null; + } + if ("file".equals(uri.getScheme())) { + return uri.getPath(); + } + final ContentResolver cr = + GeckoAppShell.getApplicationContext().getContentResolver(); + final Cursor cur = cr.query(uri, new String[] { "_data" }, /* selection */ null, + /* args */ null, /* sort */ null); + if (cur == null) { + return null; + } + try { + final int idx = cur.getColumnIndex("_data"); + if (idx < 0 || !cur.moveToFirst()) { + return null; + } + do { + try { + final String path = cur.getString(idx); + if (path != null && !path.isEmpty()) { + return path; + } + } catch (final Exception e) { + } + } while (cur.moveToNext()); + } finally { + cur.close(); + } + return null; + } + + @Override // FileCallback + public void confirm(final Uri[] uris) { + if ("single".equals(mMode) && (uris == null || uris.length != 1)) { + throw new IllegalArgumentException(); + } + if ("file".equals(mType)) { + final String[] paths = new String[uris != null ? uris.length : 0]; + for (int i = 0; i < paths.length; i++) { + paths[i] = getFile(uris[i]); + if (paths[i] == null) { + Log.e(LOGTAG, "Only file URI is supported: " + uris[i]); + } + } + ensureResult().putStringArray("files", paths); + } else { + throw new UnsupportedOperationException(); + } + submit(); + } + } + + /* package */ static void handlePromptEvent(final GeckoView view, + final GeckoBundle message, + final EventCallback callback) { + final PromptDelegate delegate; + if (view != null && view.mPromptDelegate != null) { + delegate = view.mPromptDelegate; + } else { + delegate = sDefaultPromptDelegate; + } + + if (delegate == null) { + // Default behavior is same as calling dismiss() on callback. + callback.sendSuccess(null); + return; + } + + final String type = message.getString("type"); + final String mode = message.getString("mode"); + final PromptCallback cb = new PromptCallback(type, mode, message, callback); + final String title = message.getString("title"); + final String msg = message.getString("msg"); + switch (type) { + case "alert": { + delegate.alert(view, title, msg, cb); + break; + } + case "button": { + final String[] btnTitle = message.getStringArray("btnTitle"); + final String[] btnCustomTitle = message.getStringArray("btnCustomTitle"); + for (int i = 0; i < btnCustomTitle.length; i++) { + final int resId; + if ("ok".equals(btnTitle[i])) { + resId = android.R.string.ok; + } else if ("cancel".equals(btnTitle[i])) { + resId = android.R.string.cancel; + } else if ("yes".equals(btnTitle[i])) { + resId = android.R.string.yes; + } else if ("no".equals(btnTitle[i])) { + resId = android.R.string.no; + } else { + continue; + } + btnCustomTitle[i] = Resources.getSystem().getString(resId); + } + delegate.promptForButton(view, title, msg, btnCustomTitle, cb); + break; + } + case "text": { + delegate.promptForText(view, title, msg, message.getString("value"), cb); + break; + } + case "auth": { + delegate.promptForAuth(view, title, msg, message.getBundle("options"), cb); + break; + } + case "choice": { + final int intMode; + if ("menu".equals(mode)) { + intMode = PromptDelegate.CHOICE_TYPE_MENU; + } else if ("single".equals(mode)) { + intMode = PromptDelegate.CHOICE_TYPE_SINGLE; + } else if ("multiple".equals(mode)) { + intMode = PromptDelegate.CHOICE_TYPE_MULTIPLE; + } else { + callback.sendError("Invalid mode"); + return; + } + delegate.promptForChoice(view, title, msg, intMode, + message.getBundleArray("choices"), cb); + break; + } + case "color": { + delegate.promptForColor(view, title, message.getString("value"), cb); + break; + } + case "datetime": { + final int intMode; + if ("date".equals(mode)) { + intMode = PromptDelegate.DATETIME_TYPE_DATE; + } else if ("month".equals(mode)) { + intMode = PromptDelegate.DATETIME_TYPE_MONTH; + } else if ("week".equals(mode)) { + intMode = PromptDelegate.DATETIME_TYPE_WEEK; + } else if ("time".equals(mode)) { + intMode = PromptDelegate.DATETIME_TYPE_TIME; + } else if ("datetime-local".equals(mode)) { + intMode = PromptDelegate.DATETIME_TYPE_DATETIME_LOCAL; + } else { + callback.sendError("Invalid mode"); + return; + } + delegate.promptForDateTime(view, title, intMode, + message.getString("value"), + message.getString("min"), + message.getString("max"), cb); + break; + } + case "file": { + final int intMode; + if ("single".equals(mode)) { + intMode = PromptDelegate.FILE_TYPE_SINGLE; + } else if ("multiple".equals(mode)) { + intMode = PromptDelegate.FILE_TYPE_MULTIPLE; + } else { + callback.sendError("Invalid mode"); + return; + } + String[] mimeTypes = message.getStringArray("mimeTypes"); + final String[] extensions = message.getStringArray("extension"); + if (extensions != null) { + final ArrayList combined = + new ArrayList<>(mimeTypes.length + extensions.length); + combined.addAll(Arrays.asList(mimeTypes)); + for (final String extension : extensions) { + final String mimeType = + URLConnection.guessContentTypeFromName(extension); + if (mimeType != null) { + combined.add(mimeType); + } + } + mimeTypes = combined.toArray(new String[combined.size()]); + } + delegate.promptForFile(view, title, intMode, mimeTypes, cb); + break; + } + default: { + callback.sendError("Invalid type"); + break; + } + } + } + public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) { GeckoAppShell.setGeckoInterface(geckoInterface); } @@ -594,72 +1001,6 @@ public class GeckoView extends LayerView return mEventDispatcher; } - /* Provides a means for the client to indicate whether a JavaScript - * dialog request should proceed. An instance of this class is passed to - * various GeckoViewChrome callback actions. - */ - public class PromptResult { - public PromptResult() { - } - - /** - * Handle a confirmation response from the user. - */ - public void confirm() { - } - - /** - * Handle a confirmation response from the user. - * @param value String value to return to the browser context. - */ - public void confirmWithValue(String value) { - } - - /** - * Handle a cancellation response from the user. - */ - public void cancel() { - } - } - - public interface ChromeDelegate { - /** - * Tell the host application to display an alert dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - void onAlert(GeckoView view, String message, GeckoView.PromptResult result); - - /** - * Tell the host application to display a confirmation dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - void onConfirm(GeckoView view, String message, GeckoView.PromptResult result); - - /** - * Tell the host application to display an input prompt dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param defaultValue The string to use as default input. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - void onPrompt(GeckoView view, String message, String defaultValue, GeckoView.PromptResult result); - - /** - * Tell the host application to display a remote debugging request dialog. - * @param view The GeckoView that initiated the callback. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - void onDebugRequest(GeckoView view, GeckoView.PromptResult result); - } - public interface ProgressListener { static final int STATE_IS_BROKEN = 1; static final int STATE_IS_SECURE = 2; @@ -719,4 +1060,357 @@ public class GeckoView extends LayerView */ void onCanGoForward(GeckoView view, boolean canGoForward); } + + /** + * GeckoView applications implement this interface to handle prompts triggered by + * content in the GeckoView, such as alerts, authentication dialogs, and select list + * pickers. + **/ + public interface PromptDelegate { + /** + * Callback interface for notifying the result of a prompt, and for accessing the + * optional features for prompts (e.g. optional checkbox). + */ + interface AlertCallback { + /** + * Called by the prompt implementation when the prompt is dismissed without a + * result, for example if the user presses the "Back" button. All prompts + * must call dismiss() or confirm(), if available, when the prompt is dismissed. + */ + void dismiss(); + + /** + * Return whether the prompt shown should include a checkbox. For example, if + * a page shows multiple prompts within a short period of time, the next + * prompt will include a checkbox to let the user disable future prompts. + * Although the API allows checkboxes for all prompts, in practice, only + * alert/button/text/auth prompts will possibly have a checkbox. + */ + boolean hasCheckbox(); + + /** + * Return the message label for the optional checkbox. + */ + String getCheckboxMessage(); + + /** + * Return the initial value for the optional checkbox. + */ + boolean getCheckboxValue(); + + /** + * Set the current value for the optional checkbox. + */ + void setCheckboxValue(boolean value); + } + + /** + * Display a simple message prompt. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param msg Message for the prompt dialog. + * @param callback Callback interface. + */ + void alert(GeckoView view, String title, String msg, AlertCallback callback); + + /** + * Callback interface for notifying the result of a button prompt. + */ + interface ButtonCallback extends AlertCallback { + /** + * Called by the prompt implementation when the button prompt is dismissed by + * the user pressing one of the buttons. + */ + void confirm(int button); + } + + static final int BUTTON_TYPE_POSITIVE = 0; + static final int BUTTON_TYPE_NEUTRAL = 1; + static final int BUTTON_TYPE_NEGATIVE = 2; + + /** + * Display a prompt with up to three buttons. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param msg Message for the prompt dialog. + * @param btnMsg Array of 3 elements indicating labels for the individual buttons. + * btnMsg[BUTTON_TYPE_POSITIVE] is the label for the "positive" button. + * btnMsg[BUTTON_TYPE_NEUTRAL] is the label for the "neutral" button. + * btnMsg[BUTTON_TYPE_NEGATIVE] is the label for the "negative" button. + * The button is hidden if the corresponding label is null. + * @param callback Callback interface. + */ + void promptForButton(GeckoView view, String title, String msg, + String[] btnMsg, ButtonCallback callback); + + /** + * Callback interface for notifying the result of prompts that have text results, + * including color and date/time pickers. + */ + interface TextCallback extends AlertCallback { + /** + * Called by the prompt implementation when the text prompt is confirmed by + * the user, for example by pressing the "OK" button. + */ + void confirm(String text); + } + + /** + * Display a prompt for inputting text. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param msg Message for the prompt dialog. + * @param value Default input text for the prompt. + * @param callback Callback interface. + */ + void promptForText(GeckoView view, String title, String msg, + String value, TextCallback callback); + + /** + * Callback interface for notifying the result of authentication prompts. + */ + interface AuthCallback extends AlertCallback { + /** + * Called by the prompt implementation when a password-only prompt is + * confirmed by the user. + */ + void confirm(String password); + + /** + * Called by the prompt implementation when a username/password prompt is + * confirmed by the user. + */ + void confirm(String username, String password); + } + + /** + * The auth prompt is for a network host. + */ + static final int AUTH_FLAG_HOST = 1; + /** + * The auth prompt is for a proxy. + */ + static final int AUTH_FLAG_PROXY = 2; + /** + * The auth prompt should only request a password. + */ + static final int AUTH_FLAG_ONLY_PASSWORD = 8; + /** + * The auth prompt is the result of a previous failed login. + */ + static final int AUTH_FLAG_PREVIOUS_FAILED = 16; + /** + * The auth prompt is for a cross-origin sub-resource. + */ + static final int AUTH_FLAG_CROSS_ORIGIN_SUB_RESOURCE = 32; + + /** + * The auth request is unencrypted or the encryption status is unknown. + */ + static final int AUTH_LEVEL_NONE = 0; + /** + * The auth request only encrypts password but not data. + */ + static final int AUTH_LEVEL_PW_ENCRYPTED = 1; + /** + * The auth request encrypts both password and data. + */ + static final int AUTH_LEVEL_SECURE = 2; + + /** + * Display a prompt for authentication credentials. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param msg Message for the prompt dialog. + * @param options Bundle containing options for the prompt with keys, + * "flags": int, bit field of AUTH_FLAG_* flags; + * "uri": String, URI for the auth request or null if unknown; + * "level": int, one of AUTH_LEVEL_* indicating level of encryption; + * "username": String, initial username or null if password-only; + * "password": String, intiial password; + * @param callback Callback interface. + */ + void promptForAuth(GeckoView view, String title, String msg, + GeckoBundle options, AuthCallback callback); + + /** + * Callback interface for notifying the result of menu or list choice. + */ + interface ChoiceCallback extends AlertCallback { + /** + * Called by the prompt implementation when the menu or single-choice list is + * dismissed by the user. + * + * @param id ID of the selected item. + */ + void confirm(String id); + + /** + * Called by the prompt implementation when the multiple-choice list is + * dismissed by the user. + * + * @param id IDs of the selected items. + */ + void confirm(String[] ids); + + /** + * Called by the prompt implementation when the menu or single-choice list is + * dismissed by the user. + * + * @param item Bundle representing the selected item; must be an original + * GeckoBundle object that was passed to the implementation. + */ + void confirm(GeckoBundle item); + + /** + * Called by the prompt implementation when the multiple-choice list is + * dismissed by the user. + * + * @param item Bundle array representing the selected items; must be original + * GeckoBundle objects that were passed to the implementation. + */ + void confirm(GeckoBundle[] items); + } + + /** + * Display choices in a menu that dismisses as soon as an item is chosen. + */ + static final int CHOICE_TYPE_MENU = 1; + + /** + * Display choices in a list that allows a single selection. + */ + static final int CHOICE_TYPE_SINGLE = 2; + + /** + * Display choices in a list that allows multiple selections. + */ + static final int CHOICE_TYPE_MULTIPLE = 3; + + /** + * Display a menu prompt or list prompt. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog, or null for no title. + * @param msg Message for the prompt dialog, or null for no message. + * @param type One of CHOICE_TYPE_* indicating the type of prompt. + * @param choices Array of bundles each representing an item or group, with keys, + * "disabled": boolean, true if the item should not be selectable; + * "icon": String, URI of the item icon or null if none + * (only valid for menus); + * "id": String, ID of the item or group; + * "items": GeckoBundle[], array of sub-items in a group or null + * if not a group. + * "label": String, label for displaying the item or group; + * "selected": boolean, true if the item should be pre-selected + * (pre-checked for menu items); + * "separator": boolean, true if the item should be a menu separator + * (only valid for menus); + * @param callback Callback interface. + */ + void promptForChoice(GeckoView view, String title, String msg, int type, + GeckoBundle[] choices, ChoiceCallback callback); + + /** + * Display a color prompt. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param value Initial color value in HTML color format. + * @param callback Callback interface; the result passed to confirm() must be in + * HTML color format. + */ + void promptForColor(GeckoView view, String title, String value, + TextCallback callback); + + /** + * Prompt for year, month, and day. + */ + static final int DATETIME_TYPE_DATE = 1; + + /** + * Prompt for year and month. + */ + static final int DATETIME_TYPE_MONTH = 2; + + /** + * Prompt for year and week. + */ + static final int DATETIME_TYPE_WEEK = 3; + + /** + * Prompt for hour and minute. + */ + static final int DATETIME_TYPE_TIME = 4; + + /** + * Prompt for year, month, day, hour, and minute, without timezone. + */ + static final int DATETIME_TYPE_DATETIME_LOCAL = 5; + + /** + * Display a date/time prompt. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog; currently always null. + * @param type One of DATETIME_TYPE_* indicating the type of prompt. + * @param value Initial date/time value in HTML date/time format. + * @param min Minimum date/time value in HTML date/time format. + * @param max Maximum date/time value in HTML date/time format. + * @param callback Callback interface; the result passed to confirm() must be in + * HTML date/time format. + */ + void promptForDateTime(GeckoView view, String title, int type, + String value, String min, String max, TextCallback callback); + + /** + * Callback interface for notifying the result of file prompts. + */ + interface FileCallback extends AlertCallback { + /** + * Called by the prompt implementation when the user makes a file selection in + * single-selection mode. + * + * @param uri The URI of the selected file. + */ + void confirm(Uri uri); + + /** + * Called by the prompt implementation when the user makes file selections in + * multiple-selection mode. + * + * @param uris Array of URI objects for the selected files. + */ + void confirm(Uri[] uris); + } + + static final int FILE_TYPE_SINGLE = 1; + static final int FILE_TYPE_MULTIPLE = 2; + + /** + * Display a file prompt. + * + * @param view The GeckoView that triggered the prompt + * or null if the prompt is a global prompt. + * @param title Title for the prompt dialog. + * @param type One of FILE_TYPE_* indicating the prompt type. + * @param mimeTypes Array of permissible MIME types for the selected files, in + * the form "type/subtype", where "type" and/or "subtype" can be + * "*" to indicate any value. + * @param callback Callback interface. + */ + void promptForFile(GeckoView view, String title, int type, + String[] mimeTypes, FileCallback callback); + } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java deleted file mode 100644 index 674d5fa76340..000000000000 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java +++ /dev/null @@ -1,58 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * 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/. */ - -package org.mozilla.gecko; - -import android.os.Bundle; - -public class GeckoViewChrome implements GeckoView.ChromeDelegate { - /** - * Tell the host application to display an alert dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - @Override - public void onAlert(GeckoView view, String message, GeckoView.PromptResult result) { - result.cancel(); - } - - /** - * Tell the host application to display a confirmation dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - @Override - public void onConfirm(GeckoView view, String message, GeckoView.PromptResult result) { - result.cancel(); - } - - /** - * Tell the host application to display an input prompt dialog. - * @param view The GeckoView that initiated the callback. - * @param message The string to display in the dialog. - * @param defaultValue The string to use as default input. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - @Override - public void onPrompt(GeckoView view, String message, String defaultValue, GeckoView.PromptResult result) { - result.cancel(); - } - - /** - * Tell the host application to display a remote debugging request dialog. - * @param view The GeckoView that initiated the callback. - * @param result A PromptResult used to send back the result without blocking. - * Defaults to cancel requests. - */ - @Override - public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) { - result.cancel(); - } -} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java new file mode 100644 index 000000000000..27bc91697019 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java @@ -0,0 +1,783 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.geckoview_example; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Build; +import android.text.InputType; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.InflateException; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CheckedTextView; +import android.widget.CompoundButton; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.TimePicker; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import org.mozilla.gecko.GeckoView; +import org.mozilla.gecko.util.GeckoBundle; + +final class BasicGeckoViewPrompt implements GeckoView.PromptDelegate { + protected static final String LOGTAG = "BasicGeckoViewPrompt"; + + public int filePickerRequestCode = 1; + private int mFileType; + private FileCallback mFileCallback; + + private static Activity getActivity(final GeckoView view) { + if (view != null) { + final Context context = view.getContext(); + if (context instanceof Activity) { + return (Activity) context; + } + } + return null; + } + + private AlertDialog.Builder addCheckbox(final AlertDialog.Builder builder, + ViewGroup parent, + final AlertCallback callback) { + if (!callback.hasCheckbox()) { + return builder; + } + final CheckBox checkbox = new CheckBox(builder.getContext()); + if (callback.getCheckboxMessage() != null) { + checkbox.setText(callback.getCheckboxMessage()); + } + checkbox.setChecked(callback.getCheckboxValue()); + checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton button, + final boolean checked) { + callback.setCheckboxValue(checked); + } + }); + if (parent == null) { + final int padding = getViewPadding(builder); + parent = new FrameLayout(builder.getContext()); + parent.setPadding(/* left */ padding, /* top */ 0, + /* right */ padding, /* bottom */ 0); + builder.setView(parent); + } + parent.addView(checkbox); + return builder; + } + + public void alert(final GeckoView view, final String title, final String msg, + final AlertCallback callback) { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setTitle(title) + .setMessage(msg) + .setPositiveButton(android.R.string.ok, /* onClickListener */ null) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + callback.dismiss(); + } + }); + addCheckbox(builder, /* parent */ null, callback).show(); + } + + public void promptForButton(final GeckoView view, final String title, final String msg, + final String[] btnMsg, final ButtonCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setTitle(title) + .setMessage(msg) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + callback.dismiss(); + } + }); + final DialogInterface.OnClickListener listener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + callback.confirm(BUTTON_TYPE_POSITIVE); + } else if (which == DialogInterface.BUTTON_NEUTRAL) { + callback.confirm(BUTTON_TYPE_NEUTRAL); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + callback.confirm(BUTTON_TYPE_NEGATIVE); + } else { + callback.dismiss(); + } + } + }; + if (btnMsg[BUTTON_TYPE_POSITIVE] != null) { + builder.setPositiveButton(btnMsg[BUTTON_TYPE_POSITIVE], listener); + } + if (btnMsg[BUTTON_TYPE_NEUTRAL] != null) { + builder.setNeutralButton(btnMsg[BUTTON_TYPE_NEUTRAL], listener); + } + if (btnMsg[BUTTON_TYPE_NEGATIVE] != null) { + builder.setNegativeButton(btnMsg[BUTTON_TYPE_NEGATIVE], listener); + } + addCheckbox(builder, /* parent */ null, callback).show(); + } + + private int getViewPadding(final AlertDialog.Builder builder) { + final TypedArray attr = builder.getContext().obtainStyledAttributes( + new int[] { android.R.attr.listPreferredItemPaddingLeft }); + return attr.getDimensionPixelSize(0, 1); + } + + private LinearLayout addStandardLayout(final AlertDialog.Builder builder, + final String title, final String msg, + final AlertCallback callback) { + final ScrollView scrollView = new ScrollView(builder.getContext()); + final LinearLayout container = new LinearLayout(builder.getContext()); + final int padding = getViewPadding(builder); + container.setOrientation(LinearLayout.VERTICAL); + container.setPadding(/* left */ padding, /* top */ 0, + /* right */ padding, /* bottom */ 0); + scrollView.addView(container); + builder.setTitle(title) + .setMessage(msg) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + callback.dismiss(); + } + }) + .setView(scrollView); + return container; + } + + public void promptForText(final GeckoView view, final String title, final String msg, + final String value, final TextCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LinearLayout container = addStandardLayout(builder, title, msg, callback); + final EditText editText = new EditText(builder.getContext()); + editText.setText(value); + container.addView(editText); + + builder.setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + callback.confirm(editText.getText().toString()); + } + }); + + addCheckbox(builder, container, callback).show(); + } + + public void promptForAuth(final GeckoView view, final String title, final String msg, + final GeckoBundle options, final AuthCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LinearLayout container = addStandardLayout(builder, title, msg, callback); + + final int flags = options.getInt("flags"); + final int level = options.getInt("level"); + final EditText username; + if ((flags & AUTH_FLAG_ONLY_PASSWORD) == 0) { + username = new EditText(builder.getContext()); + username.setHint(R.string.username); + username.setText(options.getString("username")); + container.addView(username); + } else { + username = null; + } + + final EditText password = new EditText(builder.getContext()); + password.setHint(R.string.password); + password.setText(options.getString("password")); + password.setInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_VARIATION_PASSWORD); + container.addView(password); + + if (level != AUTH_LEVEL_NONE) { + final ImageView secure = new ImageView(builder.getContext()); + secure.setImageResource(android.R.drawable.ic_lock_lock); + container.addView(secure); + } + + builder.setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if ((flags & AUTH_FLAG_ONLY_PASSWORD) == 0) { + callback.confirm(username.getText().toString(), + password.getText().toString()); + } else { + callback.confirm(password.getText().toString()); + } + } + }); + addCheckbox(builder, container, callback).show(); + } + + private void addChoiceItems(final int type, final ArrayAdapter list, + final GeckoBundle[] items, final String indent) { + if (type == CHOICE_TYPE_MENU) { + list.addAll(items); + return; + } + + for (final GeckoBundle item : items) { + final GeckoBundle[] children = item.getBundleArray("items"); + if (indent != null && children == null) { + item.putString("label", indent + item.getString("label", "")); + } + list.add(item); + + if (children != null) { + final String newIndent; + if (type == CHOICE_TYPE_SINGLE || type == CHOICE_TYPE_MULTIPLE) { + newIndent = (indent != null) ? indent + '\t' : "\t"; + } else { + newIndent = null; + } + addChoiceItems(type, list, children, newIndent); + } + } + } + + public void promptForChoice(final GeckoView view, final String title, final String msg, + final int type, final GeckoBundle[] choices, + final ChoiceCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + addStandardLayout(builder, title, msg, callback); + + final ArrayAdapter adapter = new ArrayAdapter( + builder.getContext(), android.R.layout.simple_list_item_1) { + private static final int TYPE_MENU_ITEM = 0; + private static final int TYPE_MENU_CHECK = 1; + private static final int TYPE_SEPARATOR = 2; + private static final int TYPE_GROUP = 3; + private static final int TYPE_SINGLE = 4; + private static final int TYPE_MULTIPLE = 5; + private static final int TYPE_COUNT = 6; + + private LayoutInflater mInflater; + private View mSeparator; + + @Override + public int getViewTypeCount() { + return TYPE_COUNT; + } + + @Override + public int getItemViewType(final int position) { + final GeckoBundle item = getItem(position); + if (item.getBoolean("separator")) { + return TYPE_SEPARATOR; + } else if (type == CHOICE_TYPE_MENU) { + return item.getBoolean("selected") ? TYPE_MENU_CHECK : TYPE_MENU_ITEM; + } else if (item.containsKey("items")) { + return TYPE_GROUP; + } else if (type == CHOICE_TYPE_SINGLE) { + return TYPE_SINGLE; + } else if (type == CHOICE_TYPE_MULTIPLE) { + return TYPE_MULTIPLE; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean isEnabled(final int position) { + final GeckoBundle item = getItem(position); + return !item.getBoolean("separator") && !item.getBoolean("disabled") && + ((type != CHOICE_TYPE_SINGLE && type != CHOICE_TYPE_MULTIPLE) || + !item.containsKey("items")); + } + + @Override + public View getView(final int position, View view, + final ViewGroup parent) { + final int itemType = getItemViewType(position); + final int layoutId; + if (itemType == TYPE_SEPARATOR) { + if (mSeparator == null) { + mSeparator = new View(getContext()); + mSeparator.setLayoutParams(new ListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType)); + final TypedArray attr = getContext().obtainStyledAttributes( + new int[] { android.R.attr.listDivider }); + mSeparator.setBackgroundResource(attr.getResourceId(0, 0)); + attr.recycle(); + } + return mSeparator; + } else if (itemType == TYPE_MENU_ITEM) { + layoutId = android.R.layout.simple_list_item_1; + } else if (itemType == TYPE_MENU_CHECK) { + layoutId = android.R.layout.simple_list_item_checked; + } else if (itemType == TYPE_GROUP) { + layoutId = android.R.layout.preference_category; + } else if (itemType == TYPE_SINGLE) { + layoutId = android.R.layout.simple_list_item_single_choice; + } else if (itemType == TYPE_MULTIPLE) { + layoutId = android.R.layout.simple_list_item_multiple_choice; + } else { + throw new UnsupportedOperationException(); + } + + if (view == null) { + if (mInflater == null) { + mInflater = LayoutInflater.from(builder.getContext()); + } + view = mInflater.inflate(layoutId, parent, false); + } + + final GeckoBundle item = getItem(position); + final TextView text = (TextView) view; + text.setEnabled(!item.getBoolean("disabled")); + text.setText(item.getString("label")); + if (view instanceof CheckedTextView) { + ((CheckedTextView) view).setChecked(item.getBoolean("selected")); + } + return view; + } + }; + addChoiceItems(type, adapter, choices, /* indent */ null); + + final ListView list = new ListView(builder.getContext()); + list.setAdapter(adapter); + if (type == CHOICE_TYPE_MULTIPLE) { + list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + } + builder.setView(list); + + final AlertDialog dialog = builder.create(); + if (type == CHOICE_TYPE_SINGLE || type == CHOICE_TYPE_MENU) { + list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(final AdapterView parent, final View v, + final int position, final long id) { + final GeckoBundle item = adapter.getItem(position); + if (type == CHOICE_TYPE_MENU) { + final GeckoBundle[] children = item.getBundleArray("items"); + if (children != null) { + // Show sub-menu. + dialog.setOnDismissListener(null); + dialog.dismiss(); + promptForChoice(view, item.getString("label"), /* msg */ null, + type, children, callback); + return; + } + } + callback.confirm(item); + dialog.dismiss(); + } + }); + } else if (type == CHOICE_TYPE_MULTIPLE) { + list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(final AdapterView parent, final View v, + final int position, final long id) { + final GeckoBundle item = adapter.getItem(position); + item.putBoolean("selected", ((CheckedTextView) v).isChecked()); + } + }); + builder.setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, + final int which) { + final int len = adapter.getCount(); + ArrayList items = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + final GeckoBundle item = adapter.getItem(i); + if (item.getBoolean("selected")) { + items.add(item.getString("id")); + } + } + callback.confirm(items.toArray(new GeckoBundle[items.size()])); + } + }); + } else { + throw new UnsupportedOperationException(); + } + dialog.show(); + } + + private static int parseColor(final String value, final int def) { + try { + return Color.parseColor(value); + } catch (final IllegalArgumentException e) { + return def; + } + } + + public void promptForColor(final GeckoView view, final String title, + final String value, final TextCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + addStandardLayout(builder, title, /* msg */ null, callback); + + final int initial = parseColor(value, /* def */ 0); + final ArrayAdapter adapter = new ArrayAdapter( + builder.getContext(), android.R.layout.simple_list_item_1) { + private LayoutInflater mInflater; + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(final int position) { + return (getItem(position) == initial) ? 1 : 0; + } + + @Override + public View getView(final int position, View view, + final ViewGroup parent) { + if (mInflater == null) { + mInflater = LayoutInflater.from(builder.getContext()); + } + final int color = getItem(position); + if (view == null) { + view = mInflater.inflate((color == initial) ? + android.R.layout.simple_list_item_checked : + android.R.layout.simple_list_item_1, parent, false); + } + view.setBackgroundResource(android.R.drawable.editbox_background); + view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY); + return view; + } + }; + + adapter.addAll(0xffff4444 /* holo_red_light */, + 0xffcc0000 /* holo_red_dark */, + 0xffffbb33 /* holo_orange_light */, + 0xffff8800 /* holo_orange_dark */, + 0xff99cc00 /* holo_green_light */, + 0xff669900 /* holo_green_dark */, + 0xff33b5e5 /* holo_blue_light */, + 0xff0099cc /* holo_blue_dark */, + 0xffaa66cc /* holo_purple */, + 0xffffffff /* white */, + 0xffaaaaaa /* lighter_gray */, + 0xff555555 /* darker_gray */, + 0xff000000 /* black */); + + final ListView list = new ListView(builder.getContext()); + list.setAdapter(adapter); + builder.setView(list); + + final AlertDialog dialog = builder.create(); + list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(final AdapterView parent, final View v, + final int position, final long id) { + callback.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position))); + dialog.dismiss(); + } + }); + dialog.show(); + } + + private static Date parseDate(final SimpleDateFormat formatter, + final String value, + final boolean defaultToNow) { + try { + if (value != null && !value.isEmpty()) { + return formatter.parse(value); + } + } catch (final ParseException e) { + } + return defaultToNow ? new Date() : null; + } + + @SuppressWarnings("deprecation") + private static void setTimePickerTime(final TimePicker picker, final Calendar cal) { + if (Build.VERSION.SDK_INT >= 23) { + picker.setHour(cal.get(Calendar.HOUR_OF_DAY)); + picker.setMinute(cal.get(Calendar.MINUTE)); + } else { + picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); + picker.setCurrentMinute(cal.get(Calendar.MINUTE)); + } + } + + @SuppressWarnings("deprecation") + private static void setCalendarTime(final Calendar cal, final TimePicker picker) { + if (Build.VERSION.SDK_INT >= 23) { + cal.set(Calendar.HOUR_OF_DAY, picker.getHour()); + cal.set(Calendar.MINUTE, picker.getMinute()); + } else { + cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour()); + cal.set(Calendar.MINUTE, picker.getCurrentMinute()); + } + } + + public void promptForDateTime(final GeckoView view, final String title, final int type, + final String value, final String min, final String max, + final TextCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + final String format; + if (type == DATETIME_TYPE_DATE) { + format = "yyyy-MM-dd"; + } else if (type == DATETIME_TYPE_MONTH) { + format = "yyyy-MM"; + } else if (type == DATETIME_TYPE_WEEK) { + format = "yyyy-'W'ww"; + } else if (type == DATETIME_TYPE_TIME) { + format = "HH:mm"; + } else if (type == DATETIME_TYPE_DATETIME_LOCAL) { + format = "yyyy-MM-dd'T'HH:mm"; + } else { + throw new UnsupportedOperationException(); + } + + final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT); + final Date minDate = parseDate(formatter, min, /* defaultToNow */ false); + final Date maxDate = parseDate(formatter, max, /* defaultToNow */ false); + final Date date = parseDate(formatter, value, /* defaultToNow */ true); + final Calendar cal = formatter.getCalendar(); + cal.setTime(date); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + final DatePicker datePicker; + if (type == DATETIME_TYPE_DATE || type == DATETIME_TYPE_MONTH || + type == DATETIME_TYPE_WEEK || type == DATETIME_TYPE_DATETIME_LOCAL) { + final int resId = builder.getContext().getResources().getIdentifier( + "date_picker_dialog", "layout", "android"); + DatePicker picker = null; + if (resId != 0) { + try { + picker = (DatePicker) inflater.inflate(resId, /* root */ null); + } catch (final ClassCastException|InflateException e) { + } + } + if (picker == null) { + picker = new DatePicker(builder.getContext()); + } + picker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), /* listener */ null); + if (minDate != null) { + picker.setMinDate(minDate.getTime()); + } + if (maxDate != null) { + picker.setMaxDate(maxDate.getTime()); + } + datePicker = picker; + } else { + datePicker = null; + } + + final TimePicker timePicker; + if (type == DATETIME_TYPE_TIME || type == DATETIME_TYPE_DATETIME_LOCAL) { + final int resId = builder.getContext().getResources().getIdentifier( + "time_picker_dialog", "layout", "android"); + TimePicker picker = null; + if (resId != 0) { + try { + picker = (TimePicker) inflater.inflate(resId, /* root */ null); + } catch (final ClassCastException|InflateException e) { + } + } + if (picker == null) { + picker = new TimePicker(builder.getContext()); + } + setTimePickerTime(picker, cal); + picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext())); + timePicker = picker; + } else { + timePicker = null; + } + + final LinearLayout container = addStandardLayout(builder, title, + /* msg */ null, callback); + container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0); + if (datePicker != null) { + container.addView(datePicker); + } + if (timePicker != null) { + container.addView(timePicker); + } + + final DialogInterface.OnClickListener listener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (which == DialogInterface.BUTTON_NEUTRAL) { + // Clear + callback.confirm(""); + return; + } + if (datePicker != null) { + cal.set(datePicker.getYear(), datePicker.getMonth(), + datePicker.getDayOfMonth()); + } + if (timePicker != null) { + setCalendarTime(cal, timePicker); + } + callback.confirm(formatter.format(cal.getTime())); + } + }; + builder.setNegativeButton(android.R.string.cancel, /* listener */ null) + .setNeutralButton(R.string.clear_field, listener) + .setPositiveButton(android.R.string.ok, listener) + .show(); + } + + public void promptForFile(GeckoView view, String title, int type, + String[] mimeTypes, FileCallback callback) + { + final Activity activity = getActivity(view); + if (activity == null) { + callback.dismiss(); + return; + } + + // Merge all given MIME types into one, using wildcard if needed. + String mimeType = null; + String mimeSubtype = null; + for (final String rawType : mimeTypes) { + final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT); + final int len = normalizedType.length(); + int slash = normalizedType.indexOf('/'); + if (slash < 0) { + slash = len; + } + final String newType = normalizedType.substring(0, slash); + final String newSubtype = normalizedType.substring(Math.min(slash + 1, len)); + if (mimeType == null) { + mimeType = newType; + } else if (!mimeType.equals(newType)) { + mimeType = "*"; + } + if (mimeSubtype == null) { + mimeSubtype = newSubtype; + } else if (!mimeSubtype.equals(newSubtype)) { + mimeSubtype = "*"; + } + } + + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType((mimeType != null ? mimeType : "*") + '/' + + (mimeSubtype != null ? mimeSubtype : "*")); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + if (type == FILE_TYPE_MULTIPLE) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + if (mimeTypes.length > 0) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + } + + try { + mFileType = type; + mFileCallback = callback; + activity.startActivityForResult(intent, filePickerRequestCode); + } catch (final ActivityNotFoundException e) { + Log.e(LOGTAG, "Cannot launch activity", e); + callback.dismiss(); + } + } + + public void onFileCallbackResult(final int resultCode, final Intent data) { + if (mFileCallback == null) { + return; + } + + final FileCallback callback = mFileCallback; + mFileCallback = null; + + if (resultCode != Activity.RESULT_OK || data == null) { + callback.dismiss(); + return; + } + + final Uri uri = data.getData(); + final ClipData clip = data.getClipData(); + + if (mFileType == FILE_TYPE_SINGLE || + (mFileType == FILE_TYPE_MULTIPLE && clip == null)) { + callback.confirm(uri); + + } else if (mFileType == FILE_TYPE_MULTIPLE) { + if (clip == null) { + Log.w(LOGTAG, "No selected file"); + callback.dismiss(); + return; + } + final int count = clip.getItemCount(); + final ArrayList uris = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + uris.add(clip.getItemAt(i).getUri()); + } + callback.confirm(uris.toArray(new Uri[uris.size()])); + } + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java index b14316fef37f..7c7873574a35 100644 --- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java @@ -6,13 +6,10 @@ package org.mozilla.geckoview_example; import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; -import android.widget.Toast; import org.mozilla.gecko.BaseGeckoInterface; import org.mozilla.gecko.GeckoProfile; @@ -25,7 +22,9 @@ public class GeckoViewActivity extends Activity { private static final String LOGTAG = "GeckoViewActivity"; private static final String DEFAULT_URL = "https://mozilla.org"; - GeckoView mGeckoView; + /* package */ static final int REQUEST_FILE_PICKER = 1; + + private GeckoView mGeckoView; @Override protected void onCreate(Bundle savedInstanceState) { @@ -36,10 +35,13 @@ public class GeckoViewActivity extends Activity { setContentView(R.layout.geckoview_activity); mGeckoView = (GeckoView) findViewById(R.id.gecko_view); - mGeckoView.setChromeDelegate(new MyGeckoViewChrome()); mGeckoView.setContentListener(new MyGeckoViewContent()); mGeckoView.setProgressListener(new MyGeckoViewProgress()); + final BasicGeckoViewPrompt prompt = new BasicGeckoViewPrompt(); + prompt.filePickerRequestCode = REQUEST_FILE_PICKER; + mGeckoView.setPromptDelegate(prompt); + final GeckoProfile profile = GeckoProfile.get(this); GeckoThread.initMainProcess(profile, /* args */ null, /* debugging */ false); @@ -63,45 +65,15 @@ public class GeckoViewActivity extends Activity { } } - private class MyGeckoViewChrome implements GeckoView.ChromeDelegate { - @Override - public void onAlert(GeckoView view, String message, GeckoView.PromptResult result) { - Log.i(LOGTAG, "Alert!"); - result.confirm(); - Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); - } - - @Override - public void onConfirm(GeckoView view, String message, final GeckoView.PromptResult result) { - Log.i(LOGTAG, "Confirm!"); - new AlertDialog.Builder(GeckoViewActivity.this) - .setTitle("javaScript dialog") - .setMessage(message) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.confirm(); - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.cancel(); - } - }) - .create() - .show(); - } - - @Override - public void onPrompt(GeckoView view, String message, String defaultValue, GeckoView.PromptResult result) { - result.cancel(); - } - - @Override - public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) { - Log.i(LOGTAG, "Remote Debug!"); - result.confirm(); + @Override + protected void onActivityResult(final int requestCode, final int resultCode, + final Intent data) { + if (requestCode == REQUEST_FILE_PICKER) { + final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt) + mGeckoView.getPromptDelegate(); + prompt.onFileCallbackResult(resultCode, data); + } else { + super.onActivityResult(requestCode, resultCode, data); } } diff --git a/mobile/android/geckoview_example/src/main/res/values/strings.xml b/mobile/android/geckoview_example/src/main/res/values/strings.xml index 1f5f447b2314..6835b71b7a9f 100644 --- a/mobile/android/geckoview_example/src/main/res/values/strings.xml +++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ geckoview_example + Username + Password + Clear diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index e81951a817c3..33e20db6b0d0 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -531,7 +531,10 @@ @BINPATH@/chrome/geckoview@JAREXT@ @BINPATH@/chrome/geckoview.manifest -#ifndef MOZ_GECKOVIEW_JAR +#ifdef MOZ_GECKOVIEW_JAR +@BINPATH@/components/GeckoView.manifest +@BINPATH@/components/GeckoViewPrompt.js +#else @BINPATH@/chrome/chrome@JAREXT@ @BINPATH@/chrome/chrome.manifest @BINPATH@/components/AboutRedirector.js diff --git a/netwerk/base/nsIPermissionManager.idl b/netwerk/base/nsIPermissionManager.idl index e666e8f4f9fd..c33399250141 100644 --- a/netwerk/base/nsIPermissionManager.idl +++ b/netwerk/base/nsIPermissionManager.idl @@ -315,6 +315,17 @@ interface nsIPermissionManager : nsISupports * @param perms An array with the permissions which match the given key. */ void setPermissionsWithKey(in ACString permissionKey, in IPCPermissionArrayRef perms); + + /** + * Broadcasts permissions for the given principal to all content processes. + * + * DO NOT USE THIS METHOD if you can avoid it. It was added in bug XXX to + * handle the current temporary implementation of ServiceWorker debugging. It + * will be removed when service worker debugging is fixed. + * + * @param aPrincipal The principal to broadcast permissions for. + */ + void broadcastPermissionsForPrincipalToAllContentProcesses(in nsIPrincipal aPrincipal); }; %{ C++ diff --git a/testing/web-platform/tests/html/webappapis/idle-callbacks/callback-multiple-calls.html b/testing/web-platform/tests/html/webappapis/idle-callbacks/callback-multiple-calls.html index 8584c71daf13..7bb524beb41f 100644 --- a/testing/web-platform/tests/html/webappapis/idle-callbacks/callback-multiple-calls.html +++ b/testing/web-platform/tests/html/webappapis/idle-callbacks/callback-multiple-calls.html @@ -5,12 +5,14 @@
diff --git a/widget/android/nsAppShell.cpp b/widget/android/nsAppShell.cpp index 4301a719eb1f..226a4a000db6 100644 --- a/widget/android/nsAppShell.cpp +++ b/widget/android/nsAppShell.cpp @@ -580,21 +580,29 @@ nsAppShell::Observe(nsISupports* aSubject, removeObserver = true; } else if (!strcmp(aTopic, "chrome-document-loaded")) { - if (jni::IsAvailable()) { - // Our first window has loaded, assume any JS initialization has run. - java::GeckoThread::CheckAndSetState( - java::GeckoThread::State::PROFILE_READY(), - java::GeckoThread::State::RUNNING()); - } - - // Enable the window event dispatcher for the given GeckoView. + // Set the global ready state and enable the window event dispatcher + // for this particular GeckoView. nsCOMPtr doc = do_QueryInterface(aSubject); MOZ_ASSERT(doc); nsCOMPtr widget = WidgetUtils::DOMWindowToWidget(doc->GetWindow()); - MOZ_ASSERT(widget); - if (widget->WindowType() == nsWindowType::eWindowType_toplevel) { - // Make sure to call this only on top level nsWindow. + + // `widget` may be one of several different types in the parent + // process, including the Android nsWindow, PuppetWidget, etc. To + // ensure that we only accept the Android nsWindow, we check that the + // widget is a top-level window and that its NS_NATIVE_WIDGET value is + // non-null, which is not the case for non-native widgets like + // PuppetWidget. + if (widget && + widget->WindowType() == nsWindowType::eWindowType_toplevel && + widget->GetNativeData(NS_NATIVE_WIDGET) == widget) { + if (jni::IsAvailable()) { + // When our first window has loaded, assume any JS + // initialization has run and set Gecko to ready. + java::GeckoThread::CheckAndSetState( + java::GeckoThread::State::PROFILE_READY(), + java::GeckoThread::State::RUNNING()); + } const auto window = static_cast(widget.get()); window->EnableEventDispatcher(); }