diff --git a/b2g/components/HelperAppDialog.js b/b2g/components/HelperAppDialog.js index cc5288acee39..6a7bb79ce8d6 100644 --- a/b2g/components/HelperAppDialog.js +++ b/b2g/components/HelperAppDialog.js @@ -33,14 +33,6 @@ HelperAppLauncherDialog.prototype = { aLauncher.saveToDisk(null, false); }, - promptForSaveToFile: function(aLauncher, - aContext, - aDefaultFile, - aSuggestedFileExt, - aForcePrompt) { - throw Cr.NS_ERROR_NOT_AVAILABLE; - }, - promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index fd4c78216370..591762709d77 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + @@ -136,7 +136,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 4551839254aa..5281141a4bea 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index d680736e1d78..9ed035d77ce7 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index f0906c80a1b1..65e88ba631a3 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 4551839254aa..5281141a4bea 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 690380b21252..509876285778 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 9bc41c551cf0..41552254402d 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index a2204dab917f..1b49e38c6b69 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { - "git_revision": "1d53fb07984298253aad64bfa4236b7167ee3d4d", + "git_revision": "ba613ae583a706131c45e885f65d428d4a541a81", "remote": "https://git.mozilla.org/releases/gaia.git", "branch": "" }, - "revision": "08a288892d8f0b41a960104150fba34f113629e6", + "revision": "4705c493adb5c766382b27e4fbff42f7447900e9", "repo_path": "integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index ba09b583dc6b..fcae18349133 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 5db023b1943b..81562ebef1f2 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 5b3132247d4e..6b3ba21fa267 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 6957beb3a3f3..cfe4afd1e979 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/browser/base/content/aboutNetError.xhtml b/browser/base/content/aboutNetError.xhtml index bfaa301c4bc5..d085b256b267 100644 --- a/browser/base/content/aboutNetError.xhtml +++ b/browser/base/content/aboutNetError.xhtml @@ -217,9 +217,15 @@ } window.addEventListener("AboutNetErrorOptions", function(evt) { - // Pinning errors are of type nssFailure2 (don't ask me why) + // Pinning errors are of type nssFailure2 if (getErrorCode() == "nssFailure2" && !errTitle.hasAttribute("sslv3")) { - // TODO: and the pref is set... + var learnMoreLink = document.getElementById("learnMoreLink"); + // nssFailure2 also gets us other non-overrideable errors. Choose + // a "learn more" link based on description: + if (getDescription().contains("mozilla_pkix_error_key_pinning_failure")) { + learnMoreLink.href = "https://support.mozilla.org/kb/certificate-pinning-reports"; + } + var options = JSON.parse(evt.detail); if (options && options.enabled) { var checkbox = document.getElementById('automaticallyReportInFuture'); @@ -476,8 +482,7 @@

- - &errorReporting.learnMore; + &errorReporting.learnMore; diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index 9acd3bb9e564..2d721e961e4c 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1268,6 +1268,9 @@ onSuccess: function(engine) { event.target.hidePopup(); BrowserSearch.searchBar.openSuggestionsPanel(); + }, + onError: function(errorCode) { + Components.utils.reportError("Error adding search engine: " + errorCode); } } Services.search.addEngine(target.getAttribute("uri"), diff --git a/browser/components/loop/content/libs/l10n.js b/browser/components/loop/content/libs/l10n.js index b3eaa63b8b67..8429fcfaaff0 100644 --- a/browser/components/loop/content/libs/l10n.js +++ b/browser/components/loop/content/libs/l10n.js @@ -40,7 +40,6 @@ function translateString(key, args, fallback) { if (args && args.num) { var num = args && args.num; - delete args.num; } var data = getL10nData(key, num); if (!data && fallback) diff --git a/browser/components/loop/test/desktop-local/index.html b/browser/components/loop/test/desktop-local/index.html index 030e8b5ba79b..36e87793f00d 100644 --- a/browser/components/loop/test/desktop-local/index.html +++ b/browser/components/loop/test/desktop-local/index.html @@ -73,6 +73,7 @@ + + + + diff --git a/browser/metro/components/HelperAppDialog.js b/browser/metro/components/HelperAppDialog.js index 257d15695273..f189db0dd14c 100644 --- a/browser/metro/components/HelperAppDialog.js +++ b/browser/metro/components/HelperAppDialog.js @@ -138,10 +138,6 @@ HelperAppLauncherDialog.prototype = { messageContainer.appendChild(fragment); }, - promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { - throw new Components.Exception("Async version must be used", Cr.NS_ERROR_NOT_AVAILABLE); - }, - promptForSaveToFileAsync: function hald_promptForSaveToFileAsync(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { let file = null; let prefs = Services.prefs; diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index bba1aeb269f8..a99dc4e786e3 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -34,6 +34,9 @@ browser.jar: skin/classic/browser/fullscreen-darknoise.png skin/classic/browser/Geolocation-16.png skin/classic/browser/Geolocation-64.png + skin/classic/browser/heartbeat-icon.svg (../shared/heartbeat-icon.svg) + skin/classic/browser/heartbeat-star-lit.svg (../shared/heartbeat-star-lit.svg) + skin/classic/browser/heartbeat-star-off.svg (../shared/heartbeat-star-off.svg) skin/classic/browser/identity.png skin/classic/browser/identity-icons-generic.png skin/classic/browser/identity-icons-https.png diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index fcbc8e27df83..64bb99f4c3e8 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -36,6 +36,9 @@ browser.jar: skin/classic/browser/Geolocation-16@2x.png skin/classic/browser/Geolocation-64.png skin/classic/browser/Geolocation-64@2x.png + skin/classic/browser/heartbeat-icon.svg (../shared/heartbeat-icon.svg) + skin/classic/browser/heartbeat-star-lit.svg (../shared/heartbeat-star-lit.svg) + skin/classic/browser/heartbeat-star-off.svg (../shared/heartbeat-star-off.svg) skin/classic/browser/identity.png skin/classic/browser/identity@2x.png skin/classic/browser/identity-icons-generic.png diff --git a/browser/themes/shared/UITour.inc.css b/browser/themes/shared/UITour.inc.css index e3c36ec1ab82..fa44dddcdf28 100644 --- a/browser/themes/shared/UITour.inc.css +++ b/browser/themes/shared/UITour.inc.css @@ -195,3 +195,110 @@ background-image: -moz-image-rect(url("chrome://browser/skin/dots@2x.png"), 0, 14, 100%, 0); } } + +/* Notification overrides for Heartbeat UI */ + +notification.heartbeat { + background-color: #F1F1F1; +%ifdef XP_MACOSX + background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%); +%endif + box-shadow: 0px 1px 0px 0px rgba(0,0,0,0.35); +} + +@keyframes pulse-onshow { + 0% { + opacity: 0; + transform: scale(1.0); + } + 25% { + opacity: 1; + transform: scale(1.1); + } + 50% { + transform: scale(1.0); + } + 75% { + transform: scale(1.1); + } + 100% { + transform: scale(1.0); + } +} + +@keyframes pulse-twice { + 0% { + transform: scale(1.1); + } + 50% { + transform: scale(0.8); + } + 100% { + transform: scale(1); + } +} + +.messageText.heartbeat { + color: #333333; + font-weight: normal; + font-family: "Lucida Grande", Segoe, Ubuntu; + font-size: 14px; + line-height: 16px; + text-shadow: none; +} + +.messageImage.heartbeat { + width: 36px; + height: 36px; + -moz-margin-end: 10px; +} + +.messageImage.heartbeat.pulse-onshow { + animation-name: pulse-onshow; + animation-duration: 1.5s; + animation-iteration-count: 1; + animation-timing-function: cubic-bezier(.7,1.8,.9,1.1); +} + +.messageImage.heartbeat.pulse-twice { + animation-name: pulse-twice; + animation-duration: 1s; + animation-iteration-count: 2; + animation-timing-function: linear; +} + +/* Heartbeat UI Rating Star Classes */ +.heartbeat > #star-rating-container { + display: -moz-box; +} + +.heartbeat > #star-rating-container > #star5 { + -moz-box-ordinal-group: 5; +} + +.heartbeat > #star-rating-container > #star4 { + -moz-box-ordinal-group: 4; +} + +.heartbeat > #star-rating-container > #star3 { + -moz-box-ordinal-group: 3; +} + +.heartbeat > #star-rating-container > #star2 { + -moz-box-ordinal-group: 2; +} + +.heartbeat > #star-rating-container > .star-x { + background: url("chrome://browser/skin/heartbeat-star-off.svg"); + cursor: pointer; + width: 24px; + height: 24px; +} + +.heartbeat > #star-rating-container > .star-x:hover, +.heartbeat > #star-rating-container > .star-x:hover ~ .star-x { + background: url("chrome://browser/skin/heartbeat-star-lit.svg"); + cursor: pointer; + width: 24px; + height: 24px; +} diff --git a/browser/themes/shared/aboutNetError.css b/browser/themes/shared/aboutNetError.css index 87c3b892f992..6a65ab4e1c5c 100644 --- a/browser/themes/shared/aboutNetError.css +++ b/browser/themes/shared/aboutNetError.css @@ -28,6 +28,7 @@ ul { } #errorPageContainer { + position: relative; min-width: 320px; max-width: 512px; } @@ -74,15 +75,10 @@ button:disabled { div#certificateErrorReporting { display: none; - float:right; + float: right; /* Align with the "Try Again" button */ - margin-top:24px; - margin-right:24px; -} - -div#certificateErrorReporting a, -div#certificateErrorReportingPanel a { - color: #0095DD; + margin-top: 24px; + -moz-margin-end: 24px; } div#certificateErrorReporting a { @@ -94,7 +90,11 @@ div#certificateErrorReporting a:hover { } span.downArrow { - font-size: 0.9em; + display: inline-block; + vertical-align: middle; + font-size: 0.6em; + -moz-margin-start: 0.5em; + transform: scaleY(0.7); } div#certificateErrorReportingPanel { @@ -106,11 +106,18 @@ div#certificateErrorReportingPanel { * makes the overall div look uneven */ padding: 0 12px 12px 12px; box-shadow: 0 0 4px #ddd; - position: relative; - width: 75%; - left: 34%; font-size: 0.9em; - top: 8px; + position: absolute; + width: 75%; + margin-top: 10px; +} + +div#certificateErrorReportingPanel:-moz-dir(ltr) { + left: 34%; +} + +div#certificateErrorReportingPanel:-moz-dir(rtl) { + right: 0; } span#hostname { diff --git a/browser/themes/shared/heartbeat-icon.svg b/browser/themes/shared/heartbeat-icon.svg new file mode 100644 index 000000000000..aada840a3807 --- /dev/null +++ b/browser/themes/shared/heartbeat-icon.svg @@ -0,0 +1,23 @@ + + + +  + Line 14 + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/shared/heartbeat-star-lit.svg b/browser/themes/shared/heartbeat-star-lit.svg new file mode 100644 index 000000000000..aecaf693d193 --- /dev/null +++ b/browser/themes/shared/heartbeat-star-lit.svg @@ -0,0 +1,428 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/shared/heartbeat-star-off.svg b/browser/themes/shared/heartbeat-star-off.svg new file mode 100644 index 000000000000..e068551cc6c5 --- /dev/null +++ b/browser/themes/shared/heartbeat-star-off.svg @@ -0,0 +1,428 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/windows/customizableui/panelUIOverlay.css b/browser/themes/windows/customizableui/panelUIOverlay.css index 1c5343fff299..7c208e6f9fe2 100644 --- a/browser/themes/windows/customizableui/panelUIOverlay.css +++ b/browser/themes/windows/customizableui/panelUIOverlay.css @@ -140,3 +140,95 @@ menu.subviewbutton > .menu-right:-moz-locale-dir(rtl) { } } %endif + +@media not all and (-moz-windows-default-theme) { + #edit-controls@inAnyPanel@ > #copy-button, + #zoom-controls@inAnyPanel@ > #zoom-reset-button, + .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton { + border: 1px solid transparent; + } + + panelview .toolbarbutton-1@buttonStateHover@, + toolbarbutton.subviewbutton@buttonStateHover@, + menu.subviewbutton@menuStateHover@, + menuitem.subviewbutton@menuStateHover@, + .widget-overflow-list .toolbarbutton-1@buttonStateHover@, + .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton@buttonStateHover@ { + border-color: ThreeDLightShadow !important; + } + + panelview:not(#PanelUI-mainView) .toolbarbutton-1@buttonStateHover@, + toolbarbutton.subviewbutton@buttonStateHover@, + menu.subviewbutton@menuStateHover@, + menuitem.subviewbutton@menuStateHover@, + .widget-overflow-list .toolbarbutton-1@buttonStateHover@ { + background-color: Highlight; + color: highlighttext; + } + + panelview .toolbarbutton-1:-moz-any(@buttonStateActive@,[checked=true]), + toolbarbutton.subviewbutton@buttonStateActive@, + menu.subviewbutton@menuStateActive@, + menuitem.subviewbutton@menuStateActive@, + .widget-overflow-list .toolbarbutton-1@buttonStateActive@, + .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton@buttonStateActive@ { + background-color: Highlight; + border-color: ThreeDLightShadow; + color: highlighttext; + box-shadow: none; + } + + panelview .toolbarbutton-1[disabled], + toolbarbutton.subviewbutton[disabled], + menu.subviewbutton[disabled], + menuitem.subviewbutton[disabled], + .widget-overflow-list .toolbarbutton-1[disabled], + .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton[disabled] { + text-shadow: none; + } + + #PanelUI-fxa-status, + #PanelUI-help, + #PanelUI-customize { + border: 1px solid transparent; + } + + #PanelUI-fxa-status:not([disabled]):hover, + #PanelUI-help:not([disabled]):hover, + #PanelUI-customize:hover, + #PanelUI-fxa-status:not([disabled]):hover:active, + #PanelUI-help:not([disabled]):hover:active, + #PanelUI-customize:hover:active { + border-color: ThreeDLightShadow; + box-shadow: none; + } + + #BMB_bookmarksPopup .menu-text, + #BMB_bookmarksPopup menupopup { + color: -moz-FieldText; + } + + #BMB_bookmarksPopup .subviewbutton[disabled=true] > .menu-text { + color: GrayText; + } + + #BMB_bookmarksPopup menupopup[placespopup=true] > hbox { + box-shadow: none; + background: -moz-field; + border: 1px solid ThreeDShadow; + } + + .subviewbutton.panel-subview-footer, + #BMB_bookmarksPopup .subviewbutton.panel-subview-footer { + color: ButtonText; + } + + .subviewbutton@menuStateHover@, + menuitem.panel-subview-footer@menuStateHover@, + .subviewbutton.panel-subview-footer@buttonStateHover@, + .subviewbutton.panel-subview-footer@buttonStateActive@, + #BMB_bookmarksPopup .panel-subview-footer@menuStateHover@ > .menu-text { + background-color: Highlight; + color: highlighttext !important; + } +} diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 335db6657119..e96e9c8f85d6 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -36,6 +36,9 @@ browser.jar: skin/classic/browser/fullscreen-darknoise.png skin/classic/browser/Geolocation-16.png skin/classic/browser/Geolocation-64.png + skin/classic/browser/heartbeat-icon.svg (../shared/heartbeat-icon.svg) + skin/classic/browser/heartbeat-star-lit.svg (../shared/heartbeat-star-lit.svg) + skin/classic/browser/heartbeat-star-off.svg (../shared/heartbeat-star-off.svg) skin/classic/browser/Info.png skin/classic/browser/identity.png skin/classic/browser/identity-icons-generic.png @@ -494,6 +497,9 @@ browser.jar: skin/classic/aero/browser/fullscreen-darknoise.png skin/classic/aero/browser/Geolocation-16.png skin/classic/aero/browser/Geolocation-64.png + skin/classic/aero/browser/heartbeat-icon.svg (../shared/heartbeat-icon.svg) + skin/classic/aero/browser/heartbeat-star-lit.svg (../shared/heartbeat-star-lit.svg) + skin/classic/aero/browser/heartbeat-star-off.svg (../shared/heartbeat-star-off.svg) skin/classic/aero/browser/Info.png (Info-aero.png) skin/classic/aero/browser/identity.png (identity-aero.png) skin/classic/aero/browser/identity-icons-generic.png diff --git a/dom/events/MouseEvent.cpp b/dom/events/MouseEvent.cpp index cf1c837ffdd6..f9b60f56b010 100644 --- a/dom/events/MouseEvent.cpp +++ b/dom/events/MouseEvent.cpp @@ -466,6 +466,12 @@ MouseEvent::GetMozPressure(float* aPressure) return NS_OK; } +bool +MouseEvent::HitCluster() const +{ + return mEvent->AsMouseEventBase()->hitCluster; +} + uint16_t MouseEvent::MozInputSource() const { diff --git a/dom/events/MouseEvent.h b/dom/events/MouseEvent.h index 18e432230534..32f606a68a61 100644 --- a/dom/events/MouseEvent.h +++ b/dom/events/MouseEvent.h @@ -83,6 +83,7 @@ public: return GetMovementPoint().y; } float MozPressure() const; + bool HitCluster() const; uint16_t MozInputSource() const; void InitNSMouseEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, diff --git a/dom/nfc/NfcContentHelper.js b/dom/nfc/NfcContentHelper.js index 976ea44d29e3..269cdabaf6f8 100644 --- a/dom/nfc/NfcContentHelper.js +++ b/dom/nfc/NfcContentHelper.js @@ -147,6 +147,10 @@ NfcContentHelper.prototype = { }, encodeNDEFRecords: function encodeNDEFRecords(records) { + if (!Array.isArray(records)) { + return null; + } + let encodedRecords = []; for (let i = 0; i < records.length; i++) { let record = records[i]; @@ -278,6 +282,16 @@ NfcContentHelper.prototype = { rfState: rfState}); }, + callDefaultFoundHandler: function callDefaultFoundHandler(sessionToken, + isP2P, + records) { + let encodedRecords = this.encodeNDEFRecords(records); + cpmm.sendAsyncMessage("NFC:CallDefaultFoundHandler", + {sessionToken: sessionToken, + isP2P: isP2P, + records: encodedRecords}); + }, + // nsIObserver observe: function observe(subject, topic, data) { if (topic == "xpcom-shutdown") { diff --git a/dom/nfc/gonk/Nfc.js b/dom/nfc/gonk/Nfc.js index fa95b262b9a9..27f6f87eda4a 100644 --- a/dom/nfc/gonk/Nfc.js +++ b/dom/nfc/gonk/Nfc.js @@ -57,7 +57,8 @@ const NFC_CID = const NFC_IPC_MSG_ENTRIES = [ { permission: null, messages: ["NFC:AddEventListener", - "NFC:QueryInfo"] }, + "NFC:QueryInfo", + "NFC:CallDefaultFoundHandler"] }, { permission: "nfc", messages: ["NFC:ReadNDEF", @@ -227,6 +228,13 @@ XPCOMUtils.defineLazyGetter(this, "gMessageManager", function () { sessionToken: sessionToken}); }, + callDefaultFoundHandler: function callDefaultFoundHandler(message) { + let sysMsg = new NfcTechDiscoveredSysMsg(message.sessionToken, + message.isP2P, + message.records || null); + gSystemMessenger.broadcastMessage("nfc-manager-tech-discovered", sysMsg); + }, + onTagFound: function onTagFound(message) { let target = this.eventListeners[this.focusApp] || this.eventListeners[NFC.SYSTEM_APP_ID]; @@ -317,6 +325,9 @@ XPCOMUtils.defineLazyGetter(this, "gMessageManager", function () { } this.nfc.sendNfcResponse(message.data); return null; + case "NFC:CallDefaultFoundHandler": + this.callDefaultFoundHandler(message.data); + return null; default: return this.nfc.receiveMessage(message); } @@ -502,11 +513,6 @@ Nfc.prototype = { } else { gMessageManager.onTagFound(message); } - - let sysMsg = new NfcTechDiscoveredSysMsg(message.sessionToken, - message.isP2P, - message.records || null); - gSystemMessenger.broadcastMessage("nfc-manager-tech-discovered", sysMsg); break; case "TechLostNotification": message.type = "techLost"; diff --git a/dom/nfc/nsINfcContentHelper.idl b/dom/nfc/nsINfcContentHelper.idl index c1ce95fb26a0..cd42489c5ef8 100644 --- a/dom/nfc/nsINfcContentHelper.idl +++ b/dom/nfc/nsINfcContentHelper.idl @@ -111,7 +111,7 @@ interface nsINfcBrowserAPI : nsISupports in boolean isFocus); }; -[scriptable, uuid(b5194ae8-d5d5-482f-a73f-dd0d755a1972)] +[scriptable, uuid(b35f4bf5-e1b8-45f4-b5d3-2ae9b6d5871e)] interface nsINfcContentHelper : nsISupports { void init(in nsIDOMWindow window); @@ -278,4 +278,19 @@ interface nsINfcContentHelper : nsISupports */ void changeRFState(in DOMString rfState, in nsINfcRequestCallback callback); + + /** + * Notify parent process to call the default tagfound or peerfound event + * handler. + * + * @param sessionToken + * Session token of this event. + * @param isP2P + * Is this a P2P Session. + * @param records + * NDEF Records. + */ + void callDefaultFoundHandler(in DOMString sessionToken, + in boolean isP2P, + in nsIVariant records); }; diff --git a/dom/nfc/nsNfc.js b/dom/nfc/nsNfc.js index f2aec7b2a476..049f49f67e68 100644 --- a/dom/nfc/nsNfc.js +++ b/dom/nfc/nsNfc.js @@ -431,18 +431,29 @@ MozNFCImpl.prototype = { }, notifyTagFound: function notifyTagFound(sessionToken, tagInfo, ndefInfo, records) { + if (!this.handleTagFound(sessionToken, tagInfo, ndefInfo, records)) { + this._nfcContentHelper.callDefaultFoundHandler(sessionToken, false, records); + }; + }, + + /** + * Handles Tag Found event. + * + * returns true if the app could process this event, false otherwise. + */ + handleTagFound: function handleTagFound(sessionToken, tagInfo, ndefInfo, records) { if (this.hasDeadWrapper()) { dump("this._window or this.__DOM_IMPL__ is a dead wrapper."); - return; + return false; } if (!this.eventService.hasListenersFor(this.__DOM_IMPL__, "tagfound")) { debug("ontagfound is not registered."); - return; + return false; } if (!this.checkPermissions(["nfc"])) { - return; + return false; } this.eventService.addSystemEventListener(this._window, "visibilitychange", @@ -465,6 +476,7 @@ MozNFCImpl.prototype = { } let eventData = { + "cancelable": true, "tag": tag, "ndefRecords": ndefRecords }; @@ -472,6 +484,15 @@ MozNFCImpl.prototype = { debug("fire ontagfound " + sessionToken); let tagEvent = new this._window.MozNFCTagEvent("tagfound", eventData); this.__DOM_IMPL__.dispatchEvent(tagEvent); + + // If defaultPrevented is false, means we need to take the default action + // for this event - redirect this event to System app. Before redirecting to + // System app, we need revoke the tag object first. + if (!tagEvent.defaultPrevented) { + this.notifyTagLost(sessionToken); + } + + return tagEvent.defaultPrevented; }, notifyTagLost: function notifyTagLost(sessionToken) { @@ -504,20 +525,31 @@ MozNFCImpl.prototype = { }, notifyPeerFound: function notifyPeerFound(sessionToken, isPeerReady) { + if (!this.handlePeerFound(sessionToken, isPeerReady)) { + this._nfcContentHelper.callDefaultFoundHandler(sessionToken, true, null); + } + }, + + /** + * Handles Peer Found/Peer Ready event. + * + * returns true if the app could process this event, false otherwise. + */ + handlePeerFound: function handlePeerFound(sessionToken, isPeerReady) { if (this.hasDeadWrapper()) { dump("this._window or this.__DOM_IMPL__ is a dead wrapper."); - return; + return false; } if (!isPeerReady && !this.eventService.hasListenersFor(this.__DOM_IMPL__, "peerfound")) { debug("onpeerfound is not registered."); - return; + return false; } let perm = isPeerReady ? ["nfc-share"] : ["nfc"]; if (!this.checkPermissions(perm)) { - return; + return false; } this.eventService.addSystemEventListener(this._window, "visibilitychange", @@ -525,12 +557,36 @@ MozNFCImpl.prototype = { let peerImpl = new MozNFCPeerImpl(this._window, sessionToken); this.nfcPeer = this._window.MozNFCPeer._create(this._window, peerImpl); - let eventData = { "peer": this.nfcPeer }; - let type = (isPeerReady) ? "peerready" : "peerfound"; - debug("fire on" + type + " " + sessionToken); - let event = new this._window.MozNFCPeerEvent(type, eventData); + let eventType; + let eventData = { + "peer": this.nfcPeer + }; + + if (isPeerReady) { + eventType = "peerready"; + } else { + eventData.cancelable = true; + eventType = "peerfound"; + } + + debug("fire on" + eventType + " " + sessionToken); + let event = new this._window.MozNFCPeerEvent(eventType, eventData); this.__DOM_IMPL__.dispatchEvent(event); + + // For peerready we don't take the default action. + if (isPeerReady) { + return true; + } + + // If defaultPrevented is false, means we need to take the default action + // for this event - redirect this event to System app. Before redirecting to + // System app, we need revoke the peer object first. + if (!event.defaultPrevented) { + this.notifyPeerLost(sessionToken); + } + + return event.defaultPrevented; }, notifyPeerLost: function notifyPeerLost(sessionToken) { diff --git a/dom/storage/DOMStorageDBThread.cpp b/dom/storage/DOMStorageDBThread.cpp index cb41e782c7b0..28d67bd8a6f2 100644 --- a/dom/storage/DOMStorageDBThread.cpp +++ b/dom/storage/DOMStorageDBThread.cpp @@ -41,7 +41,7 @@ DOMStorageDBBridge::DOMStorageDBBridge() DOMStorageDBThread::DOMStorageDBThread() : mThread(nullptr) -, mMonitor("DOMStorageThreadMonitor") +, mThreadObserver(new ThreadObserver()) , mStopIOThread(false) , mWALModeEnabled(false) , mDBReady(false) @@ -76,7 +76,7 @@ DOMStorageDBThread::Init() // Need to keep the lock to avoid setting mThread later then // the thread body executes. - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); mThread = PR_CreateThread(PR_USER_THREAD, &DOMStorageDBThread::ThreadFunc, this, PR_PRIORITY_LOW, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, @@ -98,7 +98,7 @@ DOMStorageDBThread::Shutdown() Telemetry::AutoTimer timer; { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); // After we stop, no other operations can be accepted mFlushImmediately = true; @@ -130,7 +130,7 @@ DOMStorageDBThread::SyncPreload(DOMStorageCacheBridge* aCache, bool aForceSync) if (mDBReady && mWALModeEnabled) { bool pendingTasks; { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); pendingTasks = mPendingTasks.IsScopeUpdatePending(aCache->Scope()) || mPendingTasks.IsScopeClearPending(aCache->Scope()); } @@ -157,7 +157,7 @@ DOMStorageDBThread::SyncPreload(DOMStorageCacheBridge* aCache, bool aForceSync) void DOMStorageDBThread::AsyncFlush() { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); mFlushImmediately = true; monitor.Notify(); } @@ -165,7 +165,7 @@ DOMStorageDBThread::AsyncFlush() bool DOMStorageDBThread::ShouldPreloadScope(const nsACString& aScope) { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); return mScopesHavingData.Contains(aScope); } @@ -185,14 +185,14 @@ GetScopesHavingDataEnum(nsCStringHashKey* aKey, void* aArg) void DOMStorageDBThread::GetScopesHavingData(InfallibleTArray* aScopes) { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); mScopesHavingData.EnumerateEntries(GetScopesHavingDataEnum, aScopes); } nsresult DOMStorageDBThread::InsertDBOp(DOMStorageDBThread::DBOperation* aOperation) { - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); // Sentinel to don't forget to delete the operation when we exit early. nsAutoPtr opScope(aOperation); @@ -204,7 +204,7 @@ DOMStorageDBThread::InsertDBOp(DOMStorageDBThread::DBOperation* aOperation) } if (NS_FAILED(mStatus)) { - MonitorAutoUnlock unlock(mMonitor); + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); aOperation->Finalize(mStatus); return mStatus; } @@ -225,7 +225,7 @@ DOMStorageDBThread::InsertDBOp(DOMStorageDBThread::DBOperation* aOperation) // actually been cleared from the database. Preloads are processed // immediately before update and clear operations on the database that // are flushed periodically in batches. - MonitorAutoUnlock unlock(mMonitor); + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); aOperation->Finalize(NS_OK); return NS_OK; } @@ -292,7 +292,7 @@ DOMStorageDBThread::ThreadFunc() { nsresult rv = InitDatabase(); - MonitorAutoLock lockMonitor(mMonitor); + MonitorAutoLock lockMonitor(mThreadObserver->GetMonitor()); if (NS_FAILED(rv)) { mStatus = rv; @@ -300,13 +300,32 @@ DOMStorageDBThread::ThreadFunc() return; } - while (MOZ_LIKELY(!mStopIOThread || mPreloads.Length() || mPendingTasks.HasTasks())) { + // Create an nsIThread for the current PRThread, so we can observe runnables + // dispatched to it. + nsCOMPtr thread = NS_GetCurrentThread(); + nsCOMPtr threadInternal = do_QueryInterface(thread); + MOZ_ASSERT(threadInternal); // Should always succeed. + threadInternal->SetObserver(mThreadObserver); + + while (MOZ_LIKELY(!mStopIOThread || mPreloads.Length() || + mPendingTasks.HasTasks() || + mThreadObserver->HasPendingEvents())) { + // Process xpcom events first. + while (MOZ_UNLIKELY(mThreadObserver->HasPendingEvents())) { + mThreadObserver->ClearPendingEvents(); + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + bool processedEvent; + do { + rv = thread->ProcessNextEvent(false, &processedEvent); + } while (NS_SUCCEEDED(rv) && processedEvent); + } + if (MOZ_UNLIKELY(TimeUntilFlush() == 0)) { // Flush time is up or flush has been forced, do it now. UnscheduleFlush(); if (mPendingTasks.Prepare()) { { - MonitorAutoUnlock unlockMonitor(mMonitor); + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); rv = mPendingTasks.Execute(this); } @@ -320,7 +339,7 @@ DOMStorageDBThread::ThreadFunc() nsAutoPtr op(mPreloads[0]); mPreloads.RemoveElementAt(0); { - MonitorAutoUnlock unlockMonitor(mMonitor); + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); op->PerformAndFinalize(this); } @@ -333,8 +352,41 @@ DOMStorageDBThread::ThreadFunc() } // thread loop mStatus = ShutdownDatabase(); + + if (threadInternal) { + threadInternal->SetObserver(nullptr); + } } + +NS_IMPL_ISUPPORTS(DOMStorageDBThread::ThreadObserver, nsIThreadObserver) + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::OnDispatchedEvent(nsIThreadInternal *thread) +{ + MonitorAutoLock lock(mMonitor); + mHasPendingEvents = true; + lock.Notify(); + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::OnProcessNextEvent(nsIThreadInternal *thread, + bool mayWait, + uint32_t recursionDepth) +{ + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::AfterProcessNextEvent(nsIThreadInternal *thread, + uint32_t recursionDepth, + bool eventWasProcessed) +{ + return NS_OK; +} + + extern void ReverseString(const nsCSubstring& aSource, nsCSubstring& aResult); @@ -508,7 +560,7 @@ DOMStorageDBThread::InitDatabase() rv = stmt->GetUTF8String(0, foundScope); NS_ENSURE_SUCCESS(rv, rv); - MonitorAutoLock monitor(mMonitor); + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); mScopesHavingData.PutEntry(foundScope); } @@ -648,7 +700,7 @@ DOMStorageDBThread::ScheduleFlush() mDirtyEpoch = PR_IntervalNow() | 1; // Must be non-zero to indicate we are scheduled // Wake the monitor from indefinite sleep... - mMonitor.Notify(); + (mThreadObserver->GetMonitor()).Notify(); } void diff --git a/dom/storage/DOMStorageDBThread.h b/dom/storage/DOMStorageDBThread.h index 213d71cbc232..cba7c12e5847 100644 --- a/dom/storage/DOMStorageDBThread.h +++ b/dom/storage/DOMStorageDBThread.h @@ -15,6 +15,7 @@ #include "nsCOMPtr.h" #include "nsClassHashtable.h" #include "nsIFile.h" +#include "nsIThreadInternal.h" class mozIStorageConnection; @@ -208,6 +209,34 @@ public: uint32_t mFlushFailureCount; }; + class ThreadObserver MOZ_FINAL : public nsIThreadObserver + { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + ThreadObserver() + : mHasPendingEvents(false) + , mMonitor("DOMStorageThreadMonitor") + { + } + + bool HasPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + return mHasPendingEvents; + } + void ClearPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + mHasPendingEvents = false; + } + Monitor& GetMonitor() { return mMonitor; } + + private: + virtual ~ThreadObserver() {} + bool mHasPendingEvents; + // The monitor we drive the thread with + Monitor mMonitor; + }; + public: DOMStorageDBThread(); virtual ~DOMStorageDBThread() {} @@ -250,10 +279,11 @@ private: nsCOMPtr mDatabaseFile; PRThread* mThread; - // The monitor we drive the thread with - Monitor mMonitor; + // Used to observe runnables dispatched to our thread and to monitor it. + nsRefPtr mThreadObserver; - // Flag to stop, protected by the monitor + // Flag to stop, protected by the monitor returned by + // mThreadObserver->GetMonitor(). bool mStopIOThread; // Whether WAL is enabled diff --git a/dom/webidl/MouseEvent.webidl b/dom/webidl/MouseEvent.webidl index 970ba4b165a6..090daabeb380 100644 --- a/dom/webidl/MouseEvent.webidl +++ b/dom/webidl/MouseEvent.webidl @@ -106,6 +106,8 @@ partial interface MouseEvent EventTarget? relatedTargetArg, float pressure, unsigned short inputSourceArg); + [ChromeOnly] + readonly attribute boolean hitCluster; // True when touch occurs in a cluster of links }; diff --git a/dom/webidl/MozNFC.webidl b/dom/webidl/MozNFC.webidl index 1b4ed3e712ed..c34cb5cc4e5d 100644 --- a/dom/webidl/MozNFC.webidl +++ b/dom/webidl/MozNFC.webidl @@ -90,7 +90,13 @@ interface MozNFC : EventTarget { attribute EventHandler onpeerready; /** - * This event will be fired when a NFCPeer is detected. + * This event will be fired when a NFCPeer is detected. The application has to + * be running on the foreground (decided by System app) to receive this event. + * + * The default action of this event is to dispatch the event in System app + * again, and System app will run the default UX behavior (like vibration). + * So if the application would like to cancel the event, the application + * should call event.preventDefault() or return false in this event handler. */ attribute EventHandler onpeerfound; @@ -101,7 +107,16 @@ interface MozNFC : EventTarget { attribute EventHandler onpeerlost; /** - * Ths event will be fired when a NFCTag is detected. + * This event will be fired when a NFCTag is detected. The application has to + * be running on the foreground (decided by System app) to receive this event. + * + * The default action of this event is to dispatch the event in System app + * again, and System app will run the default UX behavior (like vibration) and + * launch MozActivity to handle the content of the tag. (For example, System + * app will launch Browser if the tag contains URL). So if the application + * would like to cancel the event, i.e. in the above example, the application + * would process the URL by itself without launching Browser, the application + * should call event.preventDefault() or return false in this event handler. */ attribute EventHandler ontagfound; diff --git a/layout/base/PositionedEventTargeting.cpp b/layout/base/PositionedEventTargeting.cpp index 6fa5d0d22f9f..99c26818c864 100644 --- a/layout/base/PositionedEventTargeting.cpp +++ b/layout/base/PositionedEventTargeting.cpp @@ -75,6 +75,7 @@ struct EventRadiusPrefs bool mRegistered; bool mTouchOnly; bool mRepositionEventCoords; + bool mTouchClusterDetection; }; static EventRadiusPrefs sMouseEventRadiusPrefs; @@ -121,6 +122,9 @@ GetPrefsFor(EventClassID aEventClassID) nsPrintfCString repositionPref("ui.%s.radius.reposition", prefBranch); Preferences::AddBoolVarCache(&prefs->mRepositionEventCoords, repositionPref.get(), false); + + nsPrintfCString touchClusterPref("ui.zoomedview.enabled", prefBranch); + Preferences::AddBoolVarCache(&prefs->mTouchClusterDetection, touchClusterPref.get(), false); } return prefs; @@ -316,7 +320,8 @@ SubtractFromExposedRegion(nsRegion* aExposedRegion, const nsRegion& aRegion) static nsIFrame* GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame, const nsRect& aTargetRect, const EventRadiusPrefs* aPrefs, - nsIFrame* aRestrictToDescendants, nsTArray& aCandidates) + nsIFrame* aRestrictToDescendants, nsTArray& aCandidates, + int32_t* aElementsInCluster) { nsIFrame* bestTarget = nullptr; // Lower is better; distance is in appunits @@ -358,6 +363,8 @@ GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame, continue; } + (*aElementsInCluster)++; + // distance is in appunits float distance = ComputeDistanceFromRegion(aPointRelativeToRootFrame, region); nsIContent* content = f->GetContent(); @@ -424,10 +431,18 @@ FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent, return target; } + int32_t elementsInCluster = 0; + nsIFrame* closestClickable = GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs, - restrictToDescendants, candidates); + restrictToDescendants, candidates, &elementsInCluster); if (closestClickable) { + if (prefs->mTouchClusterDetection && elementsInCluster > 1) { + if (aEvent->mClass == eMouseEventClass) { + WidgetMouseEventBase* mouseEventBase = aEvent->AsMouseEventBase(); + mouseEventBase->hitCluster = true; + } + } target = closestClickable; } PET_LOG("Final target is %p\n", target); diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index cf941f300ce5..d236c8b0915a 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -395,6 +395,8 @@ pref("font.size.inflation.minTwips", 120); // When true, zooming will be enabled on all sites, even ones that declare user-scalable=no. pref("browser.ui.zoom.force-user-scalable", false); +pref("ui.zoomedview.enabled", false); + pref("ui.touch.radius.enabled", false); pref("ui.touch.radius.leftmm", 3); pref("ui.touch.radius.topmm", 5); diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index d174456f739d..b12ceb235168 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -955,7 +955,7 @@ public class BrowserApp extends GeckoApp if (enabled) { if (mLayerView != null) { - mLayerView.setOnMetricsChangedListener(this); + mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(this); } setToolbarMargin(0); mHomePagerContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0); @@ -963,7 +963,7 @@ public class BrowserApp extends GeckoApp // Immediately show the toolbar when disabling the dynamic // toolbar. if (mLayerView != null) { - mLayerView.setOnMetricsChangedListener(null); + mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(null); } mHomePagerContainer.setPadding(0, 0, 0, 0); if (mBrowserChrome != null) { diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index d4126fc670f5..1edaad086ec9 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -130,6 +130,8 @@ public abstract class GeckoApp private static final String LOGTAG = "GeckoApp"; private static final int ONE_DAY_MS = 1000*60*60*24; + private static final boolean ZOOMED_VIEW_ENABLED = AppConstants.NIGHTLY_BUILD; + private static enum StartupAction { NORMAL, /* normal application start */ URL, /* launched with a passed URL */ @@ -173,6 +175,7 @@ public abstract class GeckoApp private ContactService mContactService; private PromptService mPromptService; private TextSelection mTextSelection; + private ZoomedView mZoomedView; protected DoorHangerPopup mDoorHangerPopup; protected FormAssistPopup mFormAssistPopup; @@ -1578,6 +1581,11 @@ public abstract class GeckoApp (TextSelectionHandle) findViewById(R.id.caret_handle), (TextSelectionHandle) findViewById(R.id.focus_handle)); + if (ZOOMED_VIEW_ENABLED) { + ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub); + mZoomedView = (ZoomedView) stub.inflate(); + } + PrefsHelper.getPref("app.update.autodownload", new PrefsHelper.PrefHandlerBase() { @Override public void prefValue(String pref, String value) { UpdateServiceHelper.registerForUpdates(GeckoApp.this, value); @@ -2048,6 +2056,9 @@ public abstract class GeckoApp mPromptService.destroy(); if (mTextSelection != null) mTextSelection.destroy(); + if (mZoomedView != null) { + mZoomedView.destroy(); + } NotificationHelper.destroy(); IntentHelper.destroy(); GeckoNetworkManager.destroy(); diff --git a/mobile/android/base/GeckoEvent.java b/mobile/android/base/GeckoEvent.java index 146f4494527a..93bde7dec20d 100644 --- a/mobile/android/base/GeckoEvent.java +++ b/mobile/android/base/GeckoEvent.java @@ -105,7 +105,8 @@ public class GeckoEvent { TELEMETRY_UI_EVENT(44), GAMEPAD_ADDREMOVE(45), GAMEPAD_DATA(46), - LONG_PRESS(47); + LONG_PRESS(47), + ZOOMEDVIEW(48); public final int value; @@ -749,6 +750,17 @@ public class GeckoEvent { return event; } + public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) { + GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW); + event.mPoints = new Point[2]; + event.mPoints[0] = new Point(x, y); + event.mPoints[1] = new Point(bufw, bufh); + event.mX = (double) scaleFactor; + event.mMetaState = tabId; + event.mBuffer = buffer; + return event; + } + public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation) { GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED); event.mScreenOrientation = aScreenOrientation; diff --git a/mobile/android/base/ZoomedView.java b/mobile/android/base/ZoomedView.java new file mode 100644 index 000000000000..c11b02758085 --- /dev/null +++ b/mobile/android/base/ZoomedView.java @@ -0,0 +1,488 @@ +package org.mozilla.gecko; + +import java.text.DecimalFormat; + +import java.nio.ByteBuffer; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.mozglue.DirectBufferAllocator; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.BitmapFactory; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChangedListener, + LayerView.OnZoomedViewListener, GeckoEventListener { + private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName(); + + private static final int ZOOM_FACTOR = 2; + private static final int W_CAPTURED_VIEW_IN_PERCENT = 80; + private static final int H_CAPTURED_VIEW_IN_PERCENT = 50; + private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000; + private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000; + + private ImageView zoomedImageView; + private LayerView layerView; + private MotionEvent actionDownEvent; + private int viewWidth; + private int viewHeight; + private int xLastPosition; + private int yLastPosition; + private boolean shouldSetVisibleOnUpdate; + private PointF returnValue; + + private boolean stopUpdateView; + + private int lastOrientation = 0; + + private ByteBuffer buffer; + private Runnable requestRenderRunnable; + private long startTimeReRender = 0; + private long lastStartTimeReRender = 0; + + private class ZoomedViewTouchListener implements View.OnTouchListener { + private float originRawX; + private float originRawY; + private int touchState; + + @Override + public boolean onTouch(View view, MotionEvent event) { + if (layerView == null) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_MOVE: + if (moveZoomedView(event)) { + touchState = MotionEvent.ACTION_MOVE; + } + break; + + case MotionEvent.ACTION_UP: + if (touchState == MotionEvent.ACTION_MOVE) { + touchState = -1; + } else { + layerView.dispatchTouchEvent(actionDownEvent); + actionDownEvent.recycle(); + PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); + MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), + MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y, + event.getMetaState()); + layerView.dispatchTouchEvent(e); + e.recycle(); + } + break; + + case MotionEvent.ACTION_DOWN: + touchState = -1; + originRawX = event.getRawX(); + originRawY = event.getRawY(); + PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); + actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), + MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y, + event.getMetaState()); + break; + } + return true; + } + + private boolean moveZoomedView(MotionEvent event) { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams(); + if ((touchState != MotionEvent.ACTION_MOVE) && (Math.abs((int) (event.getRawX() - originRawX)) < 1) + && (Math.abs((int) (event.getRawY() - originRawY)) < 1)) { + // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position. + // In this case, the move is ignored if the delta is lower than 1 unit. + return false; + } + + float newLeftMargin = params.leftMargin + event.getRawX() - originRawX; + float newTopMargin = params.topMargin + event.getRawY() - originRawY; + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin); + originRawX = event.getRawX(); + originRawY = event.getRawY(); + return true; + } + } + + public ZoomedView(Context context) { + this(context, null, 0); + } + + public ZoomedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZoomedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + returnValue = new PointF(); + requestRenderRunnable = new Runnable() { + @Override + public void run() { + requestZoomedViewRender(); + } + }; + EventDispatcher.getInstance().registerGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress", + "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange"); + } + + void destroy() { + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress", + "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange"); + } + + // This method (onFinishInflate) is called only when the zoomed view class is used inside + // an xml structure = parentHeight) { + newLayoutParams.topMargin = (int) (parentHeight - viewHeight); + } + + if (newLeftMargin < leftMarginMin) { + newLayoutParams.leftMargin = leftMarginMin; + } else if (newLeftMargin + viewWidth > parentWidth) { + newLayoutParams.leftMargin = (int) (parentWidth - viewWidth); + } + + setLayoutParams(newLayoutParams); + PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0); + xLastPosition = Math.round(convertedPosition.x); + yLastPosition = Math.round(convertedPosition.y); + requestZoomedViewRender(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // In case of orientation change, the zoomed view update is stopped until the orientation change + // is completed. At this time, the function onMetricsChanged is called and the + // zoomed view update is restarted again. + if (lastOrientation != newConfig.orientation) { + shouldBlockUpdate(true); + lastOrientation = newConfig.orientation; + } + } + + public void refreshZoomedViewSize(ImmutableViewportMetrics viewport) { + if (layerView == null) { + return; + } + + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams(); + setCapturedSize(viewport); + moveZoomedView(viewport, params.leftMargin, params.topMargin); + } + + public void setCapturedSize(ImmutableViewportMetrics metrics) { + if (layerView == null) { + return; + } + float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight()); + viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR; + viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR; + } + + public void shouldBlockUpdate(boolean shouldBlockUpdate) { + stopUpdateView = shouldBlockUpdate; + } + + public Bitmap.Config getBitmapConfig() { + return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + } + + public void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) { + if (layerView == null) { + layerView = aLayerView; + layerView.addOnZoomedViewListener(this); + layerView.setOnMetricsChangedZoomedViewportListener(this); + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + setCapturedSize(metrics); + } + startTimeReRender = 0; + shouldSetVisibleOnUpdate = true; + moveUsingGeckoPosition(leftFromGecko, topFromGecko); + } + + public void stopZoomDisplay() { + shouldSetVisibleOnUpdate = false; + this.setVisibility(View.GONE); + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + if (layerView != null) { + layerView.setOnMetricsChangedZoomedViewportListener(null); + layerView.removeOnZoomedViewListener(this); + layerView = null; + } + } + + @Override + public void handleMessage(final String event, final JSONObject message) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + try { + if (event.equals("Gesture:nothingDoneOnLongPress") || event.equals("Gesture:clusteredLinksClicked")) { + final JSONObject clickPosition = message.getJSONObject("clickPosition"); + int left = clickPosition.getInt("x"); + int top = clickPosition.getInt("y"); + // Start to display inside the zoomedView + LayerView geckoAppLayerView = GeckoAppShell.getLayerView(); + if (geckoAppLayerView != null) { + startZoomDisplay(geckoAppLayerView, left, top); + } + } else if (event.equals("Window:Resize")) { + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + refreshZoomedViewSize(metrics); + } else if (event.equals("Content:LocationChange")) { + stopZoomDisplay(); + } + } catch (JSONException e) { + Log.e(LOGTAG, "JSON exception", e); + } + } + }); + } + + private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) { + if (layerView == null) { + return; + } + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor), + (topFromGecko * metrics.zoomFactor)); + moveZoomedView(metrics, convertedPosition.x, convertedPosition.y); + } + + @Override + public void onMetricsChanged(final ImmutableViewportMetrics viewport) { + // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient). + // Post to UI Thread to avoid Exception: + // "Only the original thread that created a view hierarchy can touch its views." + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + if (layerView == null) { + return; + } + shouldBlockUpdate(false); + refreshZoomedViewSize(viewport); + } + }); + } + + @Override + public void onPanZoomStopped() { + } + + @Override + public void updateView(ByteBuffer data) { + final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig()); + if (sb3 != null) { + data.rewind(); + try { + sb3.copyPixelsFromBuffer(data); + } catch (Exception iae) { + Log.w(LOGTAG, iae.toString()); + } + BitmapDrawable ob3 = new BitmapDrawable(getResources(), sb3); + if (zoomedImageView != null) { + zoomedImageView.setImageDrawable(ob3); + } + } + if (shouldSetVisibleOnUpdate) { + this.setVisibility(View.VISIBLE); + shouldSetVisibleOnUpdate = false; + } + lastStartTimeReRender = startTimeReRender; + startTimeReRender = 0; + } + + private void updateBufferSize() { + int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; + int capacity = viewWidth * viewHeight * pixelSize; + if (buffer == null || buffer.capacity() != capacity) { + buffer = DirectBufferAllocator.free(buffer); + buffer = DirectBufferAllocator.allocate(capacity); + } + } + + private boolean isRendering() { + return (startTimeReRender != 0); + } + + private boolean renderFrequencyTooHigh() { + return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS); + } + + @Override + public void requestZoomedViewRender() { + if (stopUpdateView) { + return; + } + // remove pending runnable + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + + // "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without + // any thing changed in the zoomed area (useless calls from the "zoomed area" point of view). + // "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity + // of the html on this area. + // To avoid to slow down the application, the 2 following cases are tested: + + // 1- Last render is still running, plan another render later. + if (isRendering()) { + // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later + // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. + // For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make + // the zoomed render on this last call. + ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); + return; + } + + // 2- Current render occurs too early, plan another render later. + if (renderFrequencyTooHigh()) { + // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later + // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. + // For a page WITH animation/video, the animation/video can be stopped, and we need to make + // the zoomed render on this last call. + ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); + return; + } + + startTimeReRender = System.nanoTime(); + // Allocate the buffer if it's the first call. + // Change the buffer size if it's not the right size. + updateBufferSize(); + + int tabId = Tabs.getInstance().getSelectedTab().getId(); + + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + PointF origin = metrics.getOrigin(); + PointF offset = metrics.getMarginOffset(); + + final int xPos = (int) (origin.x - offset.x) + xLastPosition; + final int yPos = (int) (origin.y - offset.y) + yLastPosition; + + GeckoEvent e = GeckoEvent.createZoomedViewEvent(tabId, xPos, yPos, viewWidth, + viewHeight, (float) (2.0 * metrics.zoomFactor), buffer); + GeckoAppShell.sendEventToGecko(e); + } + +} diff --git a/mobile/android/base/gfx/GeckoLayerClient.java b/mobile/android/base/gfx/GeckoLayerClient.java index ba04952fff18..3becfceb9b19 100644 --- a/mobile/android/base/gfx/GeckoLayerClient.java +++ b/mobile/android/base/gfx/GeckoLayerClient.java @@ -86,7 +86,8 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget * that because mViewportMetrics might get reassigned in between reading the different * fields. */ private volatile ImmutableViewportMetrics mViewportMetrics; - private LayerView.OnMetricsChangedListener mViewportChangeListener; + private LayerView.OnMetricsChangedListener mDynamicToolbarViewportChangeListener; + private LayerView.OnMetricsChangedListener mZoomedViewViewportChangeListener; private ZoomConstraints mZoomConstraints; @@ -853,8 +854,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget * You must hold the monitor while calling this. */ private void viewportMetricsChanged(boolean notifyGecko) { - if (mViewportChangeListener != null) { - mViewportChangeListener.onMetricsChanged(mViewportMetrics); + if (mDynamicToolbarViewportChangeListener != null) { + mDynamicToolbarViewportChangeListener.onMetricsChanged(mViewportMetrics); + } + if (mZoomedViewViewportChangeListener != null) { + mZoomedViewViewportChangeListener.onMetricsChanged(mViewportMetrics); } mView.requestRender(); @@ -910,8 +914,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget /** Implementation of PanZoomTarget */ @Override public void panZoomStopped() { - if (mViewportChangeListener != null) { - mViewportChangeListener.onPanZoomStopped(); + if (mDynamicToolbarViewportChangeListener != null) { + mDynamicToolbarViewportChangeListener.onPanZoomStopped(); + } + if (mZoomedViewViewportChangeListener != null) { + mZoomedViewViewportChangeListener.onPanZoomStopped(); } } @@ -982,8 +989,12 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget return layerPoint; } - void setOnMetricsChangedListener(LayerView.OnMetricsChangedListener listener) { - mViewportChangeListener = listener; + void setOnMetricsChangedDynamicToolbarViewportListener(LayerView.OnMetricsChangedListener listener) { + mDynamicToolbarViewportChangeListener = listener; + } + + void setOnMetricsChangedZoomedViewportListener(LayerView.OnMetricsChangedListener listener) { + mZoomedViewViewportChangeListener = listener; } public void addDrawListener(DrawListener listener) { diff --git a/mobile/android/base/gfx/LayerRenderer.java b/mobile/android/base/gfx/LayerRenderer.java index 46c5071ff6f8..89205879872e 100644 --- a/mobile/android/base/gfx/LayerRenderer.java +++ b/mobile/android/base/gfx/LayerRenderer.java @@ -26,13 +26,17 @@ import android.graphics.RectF; import android.opengl.GLES20; import android.os.SystemClock; import android.util.Log; + import org.mozilla.gecko.mozglue.JNITarget; +import org.mozilla.gecko.util.ThreadUtils; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.ArrayList; +import java.util.List; import javax.microedition.khronos.egl.EGLConfig; @@ -55,6 +59,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { private static final long NANOS_PER_MS = 1000000; private static final int NANOS_PER_SECOND = 1000000000; + private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5; + private final LayerView mView; private final ScrollbarLayer mHorizScrollLayer; private final ScrollbarLayer mVertScrollLayer; @@ -90,6 +96,10 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { private int mSampleHandle; private int mTMatrixHandle; + private List mZoomedViewListeners; + private float mViewLeft = 0.0f; + private float mViewTop = 0.0f; + // column-major matrix applied to each vertex to shift the viewport from // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by // a factor of 2 to fill up the screen @@ -158,6 +168,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { mCoordBuffer = mCoordByteBuffer.asFloatBuffer(); Tabs.registerOnTabsChangedListener(this); + mZoomedViewListeners = new ArrayList(); } private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) { @@ -185,6 +196,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { mHorizScrollLayer.destroy(); mVertScrollLayer.destroy(); Tabs.unregisterOnTabsChangedListener(this); + mZoomedViewListeners.clear(); } void onSurfaceCreated(EGLConfig config) { @@ -586,6 +598,42 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { } + public void maybeRequestZoomedViewRender(RenderContext context){ + // Concurrently update of mZoomedViewListeners should not be an issue here + // because the following line is just a short-circuit + if (mZoomedViewListeners.size() == 0) { + return; + } + + // When scrolling fast, do not request zoomed view render to avoid to slow down + // the scroll in the main view. + // Speed is estimated using the offset changes between 2 display frame calls + final float viewLeft = context.viewport.left - context.offset.x; + final float viewTop = context.viewport.top - context.offset.y; + boolean shouldWaitToRender = false; + + if (Math.abs(mViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER || + Math.abs(mViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) { + shouldWaitToRender = true; + } + + mViewLeft = viewLeft; + mViewTop = viewTop; + + if (shouldWaitToRender) { + return; + } + + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) { + listener.requestZoomedViewRender(); + } + } + }); + } + /** This function is invoked via JNI; be careful when modifying signature. */ @JNITarget public void endDrawing() { @@ -595,6 +643,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { PanningPerfAPI.recordFrameTime(); + maybeRequestZoomedViewRender(mPageContext); + /* Used by robocop for testing purposes */ IntBuffer pixelBuffer = mPixelBuffer; if (mUpdated && pixelBuffer != null) { @@ -642,4 +692,25 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { } } } + + public void updateZoomedView(final ByteBuffer data) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) { + listener.updateView(data); + } + } + }); + } + + public void addOnZoomedViewListener(LayerView.OnZoomedViewListener listener) { + ThreadUtils.assertOnUiThread(); + mZoomedViewListeners.add(listener); + } + + public void removeOnZoomedViewListener(LayerView.OnZoomedViewListener listener) { + ThreadUtils.assertOnUiThread(); + mZoomedViewListeners.remove(listener); + } } diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index 00347bd6ac4e..06c72f89bde3 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -5,7 +5,9 @@ package org.mozilla.gecko.gfx; +import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; import org.mozilla.gecko.AndroidGamepadManager; import org.mozilla.gecko.AppConstants.Versions; @@ -530,6 +532,19 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener } } + @WrapElementForJNI(allowMultithread = true, stubName = "updateZoomedView") + public static void updateZoomedView(ByteBuffer data) { + data.position(0); + LayerView layerView = GeckoAppShell.getLayerView(); + if (layerView != null) { + LayerRenderer layerRenderer = layerView.getRenderer(); + if (layerRenderer != null){ + layerRenderer.updateZoomedView(data); + } + } + return; + } + public interface Listener { void renderRequested(); void sizeChanged(int width, int height); @@ -662,7 +677,27 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener public void onPanZoomStopped(); } - public void setOnMetricsChangedListener(OnMetricsChangedListener listener) { - mLayerClient.setOnMetricsChangedListener(listener); + public void setOnMetricsChangedDynamicToolbarViewportListener(OnMetricsChangedListener listener) { + mLayerClient.setOnMetricsChangedDynamicToolbarViewportListener(listener); } + + public void setOnMetricsChangedZoomedViewportListener(OnMetricsChangedListener listener) { + mLayerClient.setOnMetricsChangedZoomedViewportListener(listener); + } + + // Public hooks for zoomed view + + public interface OnZoomedViewListener { + public void requestZoomedViewRender(); + public void updateView(ByteBuffer data); + } + + public void addOnZoomedViewListener(OnZoomedViewListener listener) { + mRenderer.addOnZoomedViewListener(listener); + } + + public void removeOnZoomedViewListener(OnZoomedViewListener listener) { + mRenderer.removeOnZoomedViewListener(listener); + } + } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 33a690437f74..acace83023a6 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -500,6 +500,7 @@ gbjar.sources += [ 'widget/ThumbnailView.java', 'widget/TwoWayView.java', 'ZoomConstraints.java', + 'ZoomedView.java', ] # The following sources are checked in to version control but # generated by a script (widget/generate_themed_views.py). If you're diff --git a/mobile/android/base/resources/layout/shared_ui_components.xml b/mobile/android/base/resources/layout/shared_ui_components.xml index eef98d80678a..948259de421e 100644 --- a/mobile/android/base/resources/layout/shared_ui_components.xml +++ b/mobile/android/base/resources/layout/shared_ui_components.xml @@ -26,6 +26,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/base/util/ThreadUtils.java b/mobile/android/base/util/ThreadUtils.java index 90043b589ee7..10a5942d0aa9 100644 --- a/mobile/android/base/util/ThreadUtils.java +++ b/mobile/android/base/util/ThreadUtils.java @@ -96,6 +96,14 @@ public final class ThreadUtils { sUiHandler.post(runnable); } + public static void postDelayedToUiThread(Runnable runnable, long timeout) { + sUiHandler.postDelayed(runnable, timeout); + } + + public static void removeCallbacksFromUiThread(Runnable runnable) { + sUiHandler.removeCallbacks(runnable); + } + public static Thread getBackgroundThread() { return sBackgroundThread; } diff --git a/mobile/android/base/widget/FadedMultiColorTextView.java b/mobile/android/base/widget/FadedMultiColorTextView.java index 1c7fdb7b830b..319bd4778c45 100644 --- a/mobile/android/base/widget/FadedMultiColorTextView.java +++ b/mobile/android/base/widget/FadedMultiColorTextView.java @@ -57,8 +57,8 @@ public class FadedMultiColorTextView extends FadedTextView { final float center = getHeight() / 2; // Shrink height of gradient to prevent it overlaying parent view border. - final float top = center - getTextSize() + 1; - final float bottom = center + getTextSize() - 1; + final float top = center - getTextSize() + 2; + final float bottom = center + getTextSize() - 2; canvas.drawRect(left, top, right, bottom, fadePaint); } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 370ef5a802c3..5bcddc04279d 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -4981,6 +4981,11 @@ var BrowserEventHandler = { return; } + this._inCluster = aEvent.hitCluster; + if (this._inCluster) { + return; // No highlight for a cluster of links + } + let uri = this._getLinkURI(target); if (uri) { try { @@ -5095,16 +5100,19 @@ var BrowserEventHandler = { Cu.reportError(e); } - // The _highlightElement was chosen after fluffing the touch events - // that led to this SingleTap, so by fluffing the mouse events, they - // should find the same target since we fluff them again below. let data = JSON.parse(aData); let {x, y} = data; - this._sendMouseEvent("mousemove", x, y); - this._sendMouseEvent("mousedown", x, y); - this._sendMouseEvent("mouseup", x, y); - + if (this._inCluster) { + this._clusterClicked(x, y); + } else { + // The _highlightElement was chosen after fluffing the touch events + // that led to this SingleTap, so by fluffing the mouse events, they + // should find the same target since we fluff them again below. + this._sendMouseEvent("mousemove", x, y); + this._sendMouseEvent("mousedown", x, y); + this._sendMouseEvent("mouseup", x, y); + } // scrollToFocusedInput does its own checks to find out if an element should be zoomed into BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser); @@ -5127,6 +5135,16 @@ var BrowserEventHandler = { } }, + _clusterClicked: function(aX, aY) { + Messaging.sendRequest({ + type: "Gesture:clusteredLinksClicked", + clickPosition: { + x: aX, + y: aY + } + }); + }, + onDoubleTap: function(aData) { let metadata = BrowserApp.selectedTab.metadata; if (!metadata.allowDoubleTapZoom) { diff --git a/mobile/android/components/HelperAppDialog.js b/mobile/android/components/HelperAppDialog.js index 97d426925a38..d2e2d10708ae 100644 --- a/mobile/android/components/HelperAppDialog.js +++ b/mobile/android/components/HelperAppDialog.js @@ -236,11 +236,6 @@ HelperAppLauncherDialog.prototype = { Services.prefs.clearUserPref(this._getPrefName(mime)); }, - promptForSaveToFile: function () { - throw new Components.Exception("Async version must be used", - Cr.NS_ERROR_NOT_AVAILABLE); - }, - promptForSaveToFileAsync: function (aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { Task.spawn(function* () { diff --git a/mobile/android/themes/core/aboutReader.css b/mobile/android/themes/core/aboutReader.css index 3f752b087d7e..f5a43faefdbe 100644 --- a/mobile/android/themes/core/aboutReader.css +++ b/mobile/android/themes/core/aboutReader.css @@ -509,14 +509,6 @@ body { background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png'); } - .step-control > .plus-button { - background-image: url('chrome://browser/skin/images/reader-plus-icon-hdpi.png'); - } - - .step-control > .minus-button { - background-image: url('chrome://browser/skin/images/reader-minus-icon-hdpi.png'); - } - .toggle-button.on { background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-hdpi.png'); } @@ -539,14 +531,6 @@ body { background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-xhdpi.png'); } - .step-control > .plus-button { - background-image: url('chrome://browser/skin/images/reader-plus-icon-xhdpi.png'); - } - - .step-control > .minus-button { - background-image: url('chrome://browser/skin/images/reader-minus-icon-xhdpi.png'); - } - .toggle-button.on { background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-xhdpi.png'); } diff --git a/mobile/android/themes/core/config.css b/mobile/android/themes/core/config.css index 5ee330af84a0..07d19c515929 100644 --- a/mobile/android/themes/core/config.css +++ b/mobile/android/themes/core/config.css @@ -74,7 +74,7 @@ body { #new-pref-toggle-button { background-position: center center; - background-image: url("chrome://browser/skin/images/reader-plus-icon-xhdpi.png"); + background-image: url("chrome://browser/skin/images/config-plus.png"); background-size: 48px 48px; height: 48px; width: 48px; diff --git a/mobile/android/themes/core/images/reader-plus-icon-xhdpi.png b/mobile/android/themes/core/images/config-plus.png similarity index 100% rename from mobile/android/themes/core/images/reader-plus-icon-xhdpi.png rename to mobile/android/themes/core/images/config-plus.png diff --git a/mobile/android/themes/core/images/reader-minus-icon-hdpi.png b/mobile/android/themes/core/images/reader-minus-icon-hdpi.png deleted file mode 100644 index 30f099e0a769..000000000000 Binary files a/mobile/android/themes/core/images/reader-minus-icon-hdpi.png and /dev/null differ diff --git a/mobile/android/themes/core/images/reader-minus-icon-mdpi.png b/mobile/android/themes/core/images/reader-minus-icon-mdpi.png deleted file mode 100644 index 04fa20d59bf5..000000000000 Binary files a/mobile/android/themes/core/images/reader-minus-icon-mdpi.png and /dev/null differ diff --git a/mobile/android/themes/core/images/reader-minus-icon-xhdpi.png b/mobile/android/themes/core/images/reader-minus-icon-xhdpi.png deleted file mode 100644 index a6e12b7c2e2f..000000000000 Binary files a/mobile/android/themes/core/images/reader-minus-icon-xhdpi.png and /dev/null differ diff --git a/mobile/android/themes/core/images/reader-plus-icon-hdpi.png b/mobile/android/themes/core/images/reader-plus-icon-hdpi.png deleted file mode 100644 index d3cd2e32ed6b..000000000000 Binary files a/mobile/android/themes/core/images/reader-plus-icon-hdpi.png and /dev/null differ diff --git a/mobile/android/themes/core/images/reader-plus-icon-mdpi.png b/mobile/android/themes/core/images/reader-plus-icon-mdpi.png deleted file mode 100644 index 193dab3fcb51..000000000000 Binary files a/mobile/android/themes/core/images/reader-plus-icon-mdpi.png and /dev/null differ diff --git a/mobile/android/themes/core/jar.mn b/mobile/android/themes/core/jar.mn index 7b89bfb9d2ec..031ff6f0bcd2 100644 --- a/mobile/android/themes/core/jar.mn +++ b/mobile/android/themes/core/jar.mn @@ -85,12 +85,7 @@ chrome.jar: skin/images/about-btn-darkgrey.png (images/about-btn-darkgrey.png) skin/images/logo-hdpi.png (images/logo-hdpi.png) skin/images/wordmark-hdpi.png (images/wordmark-hdpi.png) - skin/images/reader-plus-icon-mdpi.png (images/reader-plus-icon-mdpi.png) - skin/images/reader-plus-icon-hdpi.png (images/reader-plus-icon-hdpi.png) - skin/images/reader-plus-icon-xhdpi.png (images/reader-plus-icon-xhdpi.png) - skin/images/reader-minus-icon-mdpi.png (images/reader-minus-icon-mdpi.png) - skin/images/reader-minus-icon-hdpi.png (images/reader-minus-icon-hdpi.png) - skin/images/reader-minus-icon-xhdpi.png (images/reader-minus-icon-xhdpi.png) + skin/images/config-plus.png (images/config-plus.png) skin/images/reader-dropdown-arrow-mdpi.png (images/reader-dropdown-arrow-mdpi.png) skin/images/reader-dropdown-arrow-hdpi.png (images/reader-dropdown-arrow-hdpi.png) skin/images/reader-dropdown-arrow-xhdpi.png (images/reader-dropdown-arrow-xhdpi.png) diff --git a/services/common/hawkclient.js b/services/common/hawkclient.js index 3edf97cac41b..18c86b4ec620 100644 --- a/services/common/hawkclient.js +++ b/services/common/hawkclient.js @@ -37,24 +37,32 @@ Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config", +// log.appender.dump should be one of "Fatal", "Error", "Warn", "Info", "Config", // "Debug", "Trace" or "All". If none is specified, "Error" will be used by // default. -const PREF_LOG_LEVEL = "services.hawk.loglevel"; +// Note however that Sync will also add this log to *its* DumpAppender, so +// in a Sync context it shouldn't be necessary to adjust this - however, that +// also means error logs are likely to be dump'd twice but that's OK. +const PREF_LOG_LEVEL = "services.common.hawk.log.appender.dump"; // A pref that can be set so "sensitive" information (eg, personally // identifiable info, credentials, etc) will be logged. -const PREF_LOG_SENSITIVE_DETAILS = "services.hawk.log.sensitive"; +const PREF_LOG_SENSITIVE_DETAILS = "services.common.hawk.log.sensitive"; XPCOMUtils.defineLazyGetter(this, "log", function() { let log = Log.repository.getLogger("Hawk"); - log.addAppender(new Log.DumpAppender()); - log.level = Log.Level.Error; + // We set the log itself to "debug" and set the level from the preference to + // the appender. This allows other things to send the logs to different + // appenders, while still allowing the pref to control what is seen via dump() + log.level = Log.Level.Debug; + let appender = new Log.DumpAppender(); + log.addAppender(appender); + appender.level = Log.Level.Error; try { let level = Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING && Services.prefs.getCharPref(PREF_LOG_LEVEL); - log.level = Log.Level[level] || Log.Level.Error; + appender.level = Log.Level[level] || Log.Level.Error; } catch (e) { log.error(e); } @@ -99,12 +107,17 @@ this.HawkClient.prototype = { * @param restResponse * A RESTResponse object from a RESTRequest * - * @param errorString - * A string describing the error + * @param error + * A string or object describing the error */ - _constructError: function(restResponse, errorString) { + _constructError: function(restResponse, error) { let errorObj = { - error: errorString, + error: error, + // This object is likely to be JSON.stringify'd, but neither Error() + // objects nor Components.Exception objects do the right thing there, + // so we add a new element which is simply the .toString() version of + // the error object, so it does appear in JSON'd values. + errorString: error.toString(), message: restResponse.statusText, code: restResponse.status, errno: restResponse.status @@ -190,6 +203,12 @@ this.HawkClient.prototype = { let self = this; function _onComplete(error) { + // |error| can be either a normal caught error or an explicitly created + // Components.Exception() error. Log it now as it might not end up + // correctly in the logs by the time it's passed through _constructError. + if (error) { + log.warn("hawk request error", error); + } let restResponse = this.response; let status = restResponse.status; @@ -262,10 +281,15 @@ this.HawkClient.prototype = { }; let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); - if (method == "post" || method == "put" || method == "patch") { - request[method](payloadObj, onComplete); - } else { - request[method](onComplete); + try { + if (method == "post" || method == "put" || method == "patch") { + request[method](payloadObj, onComplete); + } else { + request[method](onComplete); + } + } catch (ex) { + log.error("Failed to make hawk request", ex); + deferred.reject(ex); } return deferred.promise; diff --git a/services/common/rest.js b/services/common/rest.js index ce0b60c08d19..98cb6a713658 100644 --- a/services/common/rest.js +++ b/services/common/rest.js @@ -458,6 +458,7 @@ RESTRequest.prototype = { if (!statusSuccess) { let message = Components.Exception("", statusCode).name; let error = Components.Exception(message, statusCode); + this._log.debug(this.method + " " + uri + " failed: " + statusCode + " - " + message); this.onComplete(error); this.onComplete = this.onProgress = null; return; diff --git a/services/common/services-common.js b/services/common/services-common.js index 4bc0cb0d13b4..106a58480596 100644 --- a/services/common/services-common.js +++ b/services/common/services-common.js @@ -9,4 +9,4 @@ pref("services.common.log.logger.rest.request", "Debug"); pref("services.common.log.logger.rest.response", "Debug"); pref("services.common.storageservice.sendVersionInfo", true); -pref("services.common.tokenserverclient.logger.level", "Info"); +pref("services.common.log.logger.tokenserverclient", "Debug"); diff --git a/services/common/tokenserverclient.js b/services/common/tokenserverclient.js index 6664c6670cea..d9e00fad0b61 100644 --- a/services/common/tokenserverclient.js +++ b/services/common/tokenserverclient.js @@ -13,13 +13,13 @@ this.EXPORTED_SYMBOLS = [ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; -Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://services-common/rest.js"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/observers.js"); -const Prefs = new Preferences("services.common.tokenserverclient."); +const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient"; /** * Represents a TokenServerClient error that occurred on the client. @@ -140,7 +140,11 @@ TokenServerClientServerError.prototype._toStringFields = function() { */ this.TokenServerClient = function TokenServerClient() { this._log = Log.repository.getLogger("Common.TokenServerClient"); - this._log.level = Log.Level[Prefs.get("logger.level")]; + let level = "Debug"; + try { + level = Services.prefs.getCharPref(PREF_LOG_LEVEL); + } catch (ex) {} + this._log.level = Log.Level[level]; } TokenServerClient.prototype = { /** @@ -404,7 +408,7 @@ TokenServerClient.prototype = { } } - this._log.debug("Successful token response: " + result.id); + this._log.debug("Successful token response"); cb(null, { id: result.id, key: result.key, diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm index 840a7a87c2b4..4dfa897a71b9 100644 --- a/services/fxaccounts/FxAccountsClient.jsm +++ b/services/fxaccounts/FxAccountsClient.jsm @@ -370,7 +370,7 @@ this.FxAccountsClient.prototype = { this, "fxaBackoffTimer" ); - } + } deferred.reject(error); } ); diff --git a/services/healthreport/healthreporter.jsm b/services/healthreport/healthreporter.jsm index 101d990f35d2..c07cb4ac6843 100644 --- a/services/healthreport/healthreporter.jsm +++ b/services/healthreport/healthreporter.jsm @@ -285,7 +285,7 @@ HealthReporterState.prototype = Object.freeze({ yield this.save(); prefs.reset(["lastSubmitID", "lastPingTime"]); } else { - this._log.warn("No prefs data found."); + this._log.debug("No prefs data found."); } }, }); diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js index e5eaa38877f4..ef8d737d22c8 100644 --- a/services/sync/modules/browserid_identity.js +++ b/services/sync/modules/browserid_identity.js @@ -185,7 +185,7 @@ this.BrowserIDManager.prototype = { // Reset the world before we do anything async. this.whenReadyToAuthenticate = Promise.defer(); this.whenReadyToAuthenticate.promise.then(null, (err) => { - this._log.error("Could not authenticate: " + err); + this._log.error("Could not authenticate", err); }); // initializeWithCurrentIdentity() can be called after the @@ -244,11 +244,11 @@ this.BrowserIDManager.prototype = { this._shouldHaveSyncKeyBundle = true; // but we probably don't have one... this.whenReadyToAuthenticate.reject(err); // report what failed... - this._log.error("Background fetch for key bundle failed: " + err); + this._log.error("Background fetch for key bundle failed", err); }); // and we are done - the fetch continues on in the background... }).then(null, err => { - this._log.error("Processing logged in account: " + err); + this._log.error("Processing logged in account", err); }); }, @@ -512,7 +512,9 @@ this.BrowserIDManager.prototype = { return true; }, - // Refresh the sync token for our user. + // Refresh the sync token for our user. Returns a promise that resolves + // with a token (which may be null in one sad edge-case), or rejects with an + // error. _fetchTokenForUser: function() { let tokenServerURI = Svc.Prefs.get("tokenServerURI"); if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems... @@ -527,18 +529,16 @@ this.BrowserIDManager.prototype = { // return null for the token - sync calling unlockAndVerifyAuthState() // before actually syncing will setup the error states if necessary. if (!this._canFetchKeys()) { - log.info("_fetchTokenForUser has no keys to use."); - return null; + return Promise.resolve(null); } - log.info("Fetching assertion and token from: " + tokenServerURI); - let maybeFetchKeys = () => { // This is called at login time and every time we need a new token - in // the latter case we already have kA and kB, so optimise that case. if (userData.kA && userData.kB) { return; } + log.info("Fetching new keys"); return this._fxaService.getKeys().then( newUserData => { userData = newUserData; @@ -565,7 +565,7 @@ this.BrowserIDManager.prototype = { } let getAssertion = () => { - log.debug("Getting an assertion"); + log.info("Getting an assertion from", tokenServerURI); let audience = Services.io.newURI(tokenServerURI, null, null).prePath; return fxa.getAssertion(audience); }; @@ -594,7 +594,7 @@ this.BrowserIDManager.prototype = { if (err.response && err.response.status === 401) { err = new AuthenticationError(err); // A hawkclient error. - } else if (err.code === 401) { + } else if (err.code && err.code === 401) { err = new AuthenticationError(err); } @@ -602,13 +602,13 @@ this.BrowserIDManager.prototype = { // properly: auth error getting assertion, auth error getting token (invalid generation // and client-state error) if (err instanceof AuthenticationError) { - this._log.error("Authentication error in _fetchTokenForUser: " + err); + this._log.error("Authentication error in _fetchTokenForUser", err); // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason. this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED; } else { - this._log.error("Non-authentication error in _fetchTokenForUser: " - + (err.message || err)); - // for now assume it is just a transient network related problem. + this._log.error("Non-authentication error in _fetchTokenForUser", err); + // for now assume it is just a transient network related problem + // (although sadly, it might also be a regular unhandled exception) this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR; } // this._authFailureReason being set to be non-null in the above if clause @@ -629,6 +629,9 @@ this.BrowserIDManager.prototype = { this._log.debug("_ensureValidToken already has one"); return Promise.resolve(); } + // reset this._token as a safety net to reduce the possibility of us + // repeatedly attempting to use an invalid token if _fetchTokenForUser throws. + this._token = null; return this._fetchTokenForUser().then( token => { this._token = token; @@ -657,7 +660,7 @@ this.BrowserIDManager.prototype = { try { cb.wait(); } catch (ex) { - this._log.error("Failed to fetch a token for authentication: " + ex); + this._log.error("Failed to fetch a token for authentication", ex); return null; } if (!this._token) { @@ -708,6 +711,17 @@ BrowserIDClusterManager.prototype = { _findCluster: function() { let endPointFromIdentityToken = function() { + // The only reason (in theory ;) that we can end up with a null token + // is when this.identity._canFetchKeys() returned false. In turn, this + // should only happen if the master-password is locked or the credentials + // storage is screwed, and in those cases we shouldn't have started + // syncing so shouldn't get here anyway. + // But better safe than sorry! To keep things clearer, throw an explicit + // exception - the message will appear in the logs and the error will be + // treated as transient. + if (!this.identity._token) { + throw new Error("Can't get a cluster URL as we can't fetch keys."); + } let endpoint = this.identity._token.endpoint; // For Sync 1.5 storage endpoints, we use the base endpoint verbatim. // However, it should end in "/" because we will extend it with @@ -742,6 +756,7 @@ BrowserIDClusterManager.prototype = { cb(null, clusterURL); }).then( null, err => { + log.info("Failed to fetch the cluster URL", err); // service.js's verifyLogin() method will attempt to fetch a cluster // URL when it sees a 401. If it gets null, it treats it as a "real" // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which diff --git a/services/sync/modules/policies.js b/services/sync/modules/policies.js index 2a36bdc11d72..80992e7eb1fe 100644 --- a/services/sync/modules/policies.js +++ b/services/sync/modules/policies.js @@ -592,8 +592,13 @@ ErrorHandler.prototype = { fapp.level = Log.Level[Svc.Prefs.get("log.appender.file.level")]; root.addAppender(fapp); - // Arrange for the FxA logs to also go to our file. - Log.repository.getLogger("FirefoxAccounts").addAppender(fapp); + // Arrange for the FxA, Hawk and TokenServer logs to also go to our appenders. + for (let extra of ["FirefoxAccounts", "Hawk", "Common.TokenServerClient"]) { + let log = Log.repository.getLogger(extra); + for (let appender of [fapp, dapp, capp]) { + log.addAppender(appender); + } + } }, observe: function observe(subject, topic, data) { diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js index 4cf39d93fffe..961a32fb8555 100644 --- a/services/sync/tests/unit/test_browserid_identity.js +++ b/services/sync/tests/unit/test_browserid_identity.js @@ -95,6 +95,7 @@ add_task(function test_initialializeWithNoKeys() { yield browseridManager.whenReadyToAuthenticate.promise; do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys"); do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys"); + do_check_eq(browseridManager._token, null, "we don't have a token"); }); add_test(function test_getResourceAuthenticator() { diff --git a/storage/src/mozStorageConnection.cpp b/storage/src/mozStorageConnection.cpp index 59e91f42f27a..79f03c77c9d7 100644 --- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -51,6 +51,19 @@ PRLogModuleInfo* gStorageLog = nullptr; #endif +// Checks that the protected code is running on the main-thread only if the +// connection was also opened on it. +#ifdef DEBUG +#define CHECK_MAINTHREAD_ABUSE() \ + do { \ + nsCOMPtr mainThread = do_GetMainThread(); \ + NS_WARN_IF_FALSE(threadOpenedOn == mainThread || !NS_IsMainThread(), \ + "Using Storage synchronous API on main-thread, but the connection was opened on another thread."); \ + } while(0) +#else +#define CHECK_MAINTHREAD_ABUSE() do { /* Nothing */ } while(0) +#endif + namespace mozilla { namespace storage { @@ -1422,6 +1435,7 @@ Connection::CreateAsyncStatement(const nsACString &aSQLStatement, NS_IMETHODIMP Connection::ExecuteSimpleSQL(const nsACString &aSQLStatement) { + CHECK_MAINTHREAD_ABUSE(); if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; int srv = executeSql(mDBConn, PromiseFlatCString(aSQLStatement).get()); diff --git a/storage/src/mozStorageService.cpp b/storage/src/mozStorageService.cpp index 2ed3a362d7a3..2cf9671c6741 100644 --- a/storage/src/mozStorageService.cpp +++ b/storage/src/mozStorageService.cpp @@ -347,22 +347,32 @@ Service::minimizeMemory() for (uint32_t i = 0; i < connections.Length(); i++) { nsRefPtr conn = connections[i]; - if (conn->connectionReady()) { - NS_NAMED_LITERAL_CSTRING(shrinkPragma, "PRAGMA shrink_memory"); - nsCOMPtr syncConn = do_QueryInterface( - NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, conn)); - DebugOnly rv; + if (!conn->connectionReady()) + continue; - if (!syncConn) { - nsCOMPtr ps; - rv = connections[i]->ExecuteSimpleSQLAsync(shrinkPragma, nullptr, - getter_AddRefs(ps)); - } else { - rv = connections[i]->ExecuteSimpleSQL(shrinkPragma); - } + NS_NAMED_LITERAL_CSTRING(shrinkPragma, "PRAGMA shrink_memory"); + nsCOMPtr syncConn = do_QueryInterface( + NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, conn)); + bool onOpenedThread = false; - MOZ_ASSERT(NS_SUCCEEDED(rv), - "Should have been able to purge sqlite caches"); + if (!syncConn) { + // This is a mozIStorageAsyncConnection, it can only be used on the main + // thread, so we can do a straight API call. + nsCOMPtr ps; + DebugOnly rv = + conn->ExecuteSimpleSQLAsync(shrinkPragma, nullptr, getter_AddRefs(ps)); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should have purged sqlite caches"); + } else if (NS_SUCCEEDED(conn->threadOpenedOn->IsOnCurrentThread(&onOpenedThread)) && + onOpenedThread) { + // We are on the opener thread, so we can just proceed. + conn->ExecuteSimpleSQL(shrinkPragma); + } else { + // We are on the wrong thread, the query should be executed on the + // opener thread, so we must dispatch to it. + nsCOMPtr event = + NS_NewRunnableMethodWithArg( + conn, &Connection::ExecuteSimpleSQL, shrinkPragma); + conn->threadOpenedOn->Dispatch(event, NS_DISPATCH_NORMAL); } } } diff --git a/toolkit/components/downloads/test/unit/downloads_manifest.js b/toolkit/components/downloads/test/unit/downloads_manifest.js index a24622a6d265..7bef4117c14c 100644 --- a/toolkit/components/downloads/test/unit/downloads_manifest.js +++ b/toolkit/components/downloads/test/unit/downloads_manifest.js @@ -15,9 +15,7 @@ HelperAppDlg.prototype = { show: function (launcher, ctx, reason) { launcher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk; launcher.launchWithApplication(null, false); - }, - - promptForSaveToFile: function (launcher, ctx, defaultFile, suggestedExtension, forcePrompt) { } + } } diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js index 8a643f174c7b..33425f11507d 100644 --- a/toolkit/components/jsdownloads/test/unit/head.js +++ b/toolkit/components/jsdownloads/test/unit/head.js @@ -793,15 +793,6 @@ add_task(function test_common_initialize() createInstance: function (aOuter, aIid) { return { QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), - promptForSaveToFile: function (aLauncher, aWindowContext, - aDefaultFileName, - aSuggestedFileExtension, - aForcePrompt) - { - throw new Components.Exception( - "Synchronous promptForSaveToFile not implemented.", - Cr.NS_ERROR_NOT_AVAILABLE); - }, promptForSaveToFileAsync: function (aLauncher, aWindowContext, aDefaultFileName, aSuggestedFileExtension, diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.js b/toolkit/mozapps/downloads/nsHelperAppDlg.js index b90d4619c318..b16561a9ae1c 100644 --- a/toolkit/mozapps/downloads/nsHelperAppDlg.js +++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js @@ -195,22 +195,6 @@ nsUnknownContentTypeDialog.prototype = { bundle.GetStringFromName("badPermissions")); }, - // promptForSaveToFile: Display file picker dialog and return selected file. - // This is called by the External Helper App Service - // after the ucth dialog calls |saveToDisk| with a null - // target filename (no target, therefore user must pick). - // - // Alternatively, if the user has selected to have all - // files download to a specific location, return that - // location and don't ask via the dialog. - // - // Note - this function is called without a dialog, so it cannot access any part - // of the dialog XUL as other functions on this object do. - - promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) { - throw new Components.Exception("Async version must be used", Components.results.NS_ERROR_NOT_AVAILABLE); - }, - promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) { var result = null; diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp index 9e1a237fcdb8..6f87a873f298 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -2237,24 +2237,16 @@ void nsExternalAppHandler::RequestSaveDestination(const nsAFlatString &aDefaultF // picker is up would cause Cancel() to be called, and the dialog would be // released, which would release this object too, which would crash. // See Bug 249143 - nsIFile* fileToUse; nsRefPtr kungFuDeathGrip(this); nsCOMPtr dlg(mDialog); - rv = mDialog->PromptForSaveToFile(this, - GetDialogParent(), - aDefaultFile.get(), - aFileExtension.get(), - mForceSave, &fileToUse); - if (rv == NS_ERROR_NOT_AVAILABLE) { - // we need to use the async version -> nsIHelperAppLauncherDialog.promptForSaveToFileAsync. - rv = mDialog->PromptForSaveToFileAsync(this, - GetDialogParent(), - aDefaultFile.get(), - aFileExtension.get(), - mForceSave); - } else { - SaveDestinationAvailable(rv == NS_OK ? fileToUse : nullptr); + rv = mDialog->PromptForSaveToFileAsync(this, + GetDialogParent(), + aDefaultFile.get(), + aFileExtension.get(), + mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); } } diff --git a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl index d15f13321258..f8190e744bdf 100644 --- a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl +++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl @@ -21,7 +21,7 @@ interface nsIFile; * will access methods of the nsIHelperAppLauncher passed in to show() * in order to cause a "save to disk" or "open using" action. */ -[scriptable, uuid(3ae4dca8-ac91-4891-adcf-3fbebed6170e)] +[scriptable, uuid(bfc739f3-8d75-4034-a6f8-1039a5996bad)] interface nsIHelperAppLauncherDialog : nsISupports { /** * This request is passed to the helper app dialog because Gecko can not @@ -57,32 +57,6 @@ interface nsIHelperAppLauncherDialog : nsISupports { in nsISupports aWindowContext, in unsigned long aReason); - /** - * Invoke a save-to-file dialog instead of the full fledged helper app dialog. - * Returns the a nsIFile for the file name/location selected. - * - * @param aLauncher - * A nsIHelperAppLauncher to be invoked when a file is selected. - * @param aWindowContext - * Window associated with action. - * @param aDefaultFileName - * Default file name to provide (can be null) - * @param aSuggestedFileExtension - * Sugested file extension - * @param aForcePrompt - * Set to true to force prompting the user for thet file - * name/location, otherwise perferences may control if the user is - * prompted. - * - * @throws NS_ERROR_NOT_AVAILABLE if the async version of this function - * should be used. - */ - nsIFile promptForSaveToFile(in nsIHelperAppLauncher aLauncher, - in nsISupports aWindowContext, - in wstring aDefaultFileName, - in wstring aSuggestedFileExtension, - in boolean aForcePrompt); - /** * Async invoke a save-to-file dialog instead of the full fledged helper app * dialog. When the file is chosen (or the dialog is closed), the callback diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml index 6363e5523de6..3e5fa41c82e5 100644 --- a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml +++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml @@ -84,9 +84,6 @@ function load() { "The filename should be correctly sanitized"); gCallback(); }, - promptForSaveToFile: function(aLauncher, aWindowContext, aDefaultFileName, aSuggestedFileExtension, aForcePrompt) { - return null; - }, QueryInterface: function(aIID) { netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); if (aIID.equals(SpecialPowers.Ci.nsISupports) || diff --git a/uriloader/exthandler/tests/unit_ipc/test_encoding.js b/uriloader/exthandler/tests/unit_ipc/test_encoding.js index ea6a98192c5f..a46c899445a7 100644 --- a/uriloader/exthandler/tests/unit_ipc/test_encoding.js +++ b/uriloader/exthandler/tests/unit_ipc/test_encoding.js @@ -61,9 +61,7 @@ HelperAppDlg.prototype = { show: function (launcher, ctx, reason, usePrivateUI) { launcher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToFile; launcher.launchWithApplication(null, false); - }, - - promptForSaveToFile: function (launcher, ctx, defaultFile, suggestedExtension, forcePrompt) { } + } } // Stolen from XPCOMUtils, since this handy function is not public there diff --git a/widget/MouseEvents.h b/widget/MouseEvents.h index b3efd3d1c57c..bb6c6c77fad1 100644 --- a/widget/MouseEvents.h +++ b/widget/MouseEvents.h @@ -83,6 +83,7 @@ protected: , button(0) , buttons(0) , pressure(0) + , hitCluster(false) , inputSource(nsIDOMMouseEvent::MOZ_SOURCE_MOUSE) { } @@ -127,6 +128,8 @@ public: // Finger or touch pressure of event. It ranges between 0.0 and 1.0. float pressure; + // Touch near a cluster of links (true) + bool hitCluster; // Possible values at nsIDOMMouseEvent uint16_t inputSource; @@ -143,6 +146,7 @@ public: button = aEvent.button; buttons = aEvent.buttons; pressure = aEvent.pressure; + hitCluster = aEvent.hitCluster; inputSource = aEvent.inputSource; } diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp index 1116b1563f09..6c11f7689805 100644 --- a/widget/android/AndroidBridge.cpp +++ b/widget/android/AndroidBridge.cpp @@ -1704,6 +1704,91 @@ AndroidBridge::GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId, return true; } +static float +GetScaleFactor(nsPresContext* mPresContext) { + nsIPresShell* presShell = mPresContext->PresShell(); + LayoutDeviceToLayerScale cumulativeResolution(presShell->GetCumulativeResolution().width); + return cumulativeResolution.scale; +} + +nsresult +AndroidBridge::CaptureZoomedView (nsIDOMWindow *window, nsIntRect zoomedViewRect, Object::Param buffer, + float zoomFactor) { + nsresult rv; + struct timeval timeStart; + gettimeofday (&timeStart, NULL); + + if (!buffer) + return NS_ERROR_FAILURE; + + nsCOMPtr < nsIDOMWindowUtils > utils = do_GetInterface (window); + if (!utils) + return NS_ERROR_FAILURE; + + JNIEnv* env = GetJNIEnv (); + + AutoLocalJNIFrame jniFrame (env, 0); + + nsCOMPtr < nsPIDOMWindow > win = do_QueryInterface (window); + if (!win) { + return NS_ERROR_FAILURE; + } + nsRefPtr < nsPresContext > presContext; + + nsIDocShell* docshell = win->GetDocShell (); + + if (docshell) { + docshell->GetPresContext (getter_AddRefs (presContext)); + } + + if (!presContext) { + return NS_ERROR_FAILURE; + } + nsCOMPtr < nsIPresShell > presShell = presContext->PresShell (); + + float scaleFactor = GetScaleFactor(presContext) ; + + nscolor bgColor = NS_RGB (255, 255, 255); + uint32_t renderDocFlags = (nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING | nsIPresShell::RENDER_DOCUMENT_RELATIVE); + nsRect r (presContext->DevPixelsToAppUnits(zoomedViewRect.x / scaleFactor), + presContext->DevPixelsToAppUnits(zoomedViewRect.y / scaleFactor ), + presContext->DevPixelsToAppUnits(zoomedViewRect.width / scaleFactor ), + presContext->DevPixelsToAppUnits(zoomedViewRect.height / scaleFactor )); + + bool is24bit = (GetScreenDepth () == 24); + SurfaceFormat format = is24bit ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::R5G6B5; + gfxImageFormat iFormat = gfx::SurfaceFormatToImageFormat(format); + uint32_t stride = gfxASurface::FormatStrideForWidth(iFormat, zoomedViewRect.width); + + uint8_t* data = static_cast (env->GetDirectBufferAddress (buffer.Get())); + if (!data) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT (gfxPlatform::GetPlatform ()->SupportsAzureContentForType (BackendType::CAIRO), + "Need BackendType::CAIRO support"); + RefPtr < DrawTarget > dt = Factory::CreateDrawTargetForData ( + BackendType::CAIRO, data, IntSize (zoomedViewRect.width, zoomedViewRect.height), stride, + format); + if (!dt) { + ALOG_BRIDGE ("Error creating DrawTarget"); + return NS_ERROR_FAILURE; + } + nsRefPtr < gfxContext > context = new gfxContext (dt); + context->SetMatrix (context->CurrentMatrix ().Scale(zoomFactor, zoomFactor)); + + rv = presShell->RenderDocument (r, renderDocFlags, bgColor, context); + + if (is24bit) { + gfxUtils::ConvertBGRAtoRGBA (data, stride * zoomedViewRect.height); + } + + LayerView::updateZoomedView(buffer); + + NS_ENSURE_SUCCESS (rv, rv); + return NS_OK; +} + nsresult AndroidBridge::CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, Object::Param buffer, bool &shouldStore) { nsresult rv; diff --git a/widget/android/AndroidBridge.h b/widget/android/AndroidBridge.h index 563a1e3186e8..5e22bdb1f244 100644 --- a/widget/android/AndroidBridge.h +++ b/widget/android/AndroidBridge.h @@ -188,6 +188,7 @@ public: bool GetThreadNameJavaProfiling(uint32_t aThreadId, nsCString & aResult); bool GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId, uint32_t aFrameId, nsCString & aResult); + nsresult CaptureZoomedView(nsIDOMWindow *window, nsIntRect zoomedViewRect, jni::Object::Param buffer, float zoomFactor); nsresult CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, jni::Object::Param buffer, bool &shouldStore); void GetDisplayPort(bool aPageSizeUpdate, bool aIsBrowserContentDisplayed, int32_t tabId, nsIAndroidViewport* metrics, nsIAndroidDisplayport** displayPort); void ContentDocumentChanged(); diff --git a/widget/android/AndroidJavaWrappers.cpp b/widget/android/AndroidJavaWrappers.cpp index 5e61c62c2a8d..838567fcfbd7 100644 --- a/widget/android/AndroidJavaWrappers.cpp +++ b/widget/android/AndroidJavaWrappers.cpp @@ -538,6 +538,14 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj) break; } + case ZOOMEDVIEW: { + mX = jenv->GetDoubleField(jobj, jXField); + mMetaState = jenv->GetIntField(jobj, jMetaStateField); + ReadPointArray(mPoints, jenv, jPoints, 2); + mByteBuffer = new RefCountedJavaObject(jenv, jenv->GetObjectField(jobj, jByteBufferField)); + break; + } + case SCREENORIENTATION_CHANGED: { mScreenOrientation = jenv->GetShortField(jobj, jScreenOrientationField); break; diff --git a/widget/android/AndroidJavaWrappers.h b/widget/android/AndroidJavaWrappers.h index 7bab4816580a..48f12f0d3557 100644 --- a/widget/android/AndroidJavaWrappers.h +++ b/widget/android/AndroidJavaWrappers.h @@ -746,6 +746,7 @@ public: GAMEPAD_ADDREMOVE = 45, GAMEPAD_DATA = 46, LONG_PRESS = 47, + ZOOMEDVIEW = 48, dummy_java_enum_list_end }; diff --git a/widget/android/GeneratedJNIWrappers.cpp b/widget/android/GeneratedJNIWrappers.cpp index 16b66f5fa9c7..235e8e350bf2 100644 --- a/widget/android/GeneratedJNIWrappers.cpp +++ b/widget/android/GeneratedJNIWrappers.cpp @@ -980,6 +980,14 @@ mozilla::jni::Object::LocalRef LayerView::RegisterCompositorWrapper() return mozilla::jni::Method::Call(nullptr, nullptr); } +constexpr char LayerView::updateZoomedView_t::name[]; +constexpr char LayerView::updateZoomedView_t::signature[]; + +void LayerView::updateZoomedView(mozilla::jni::Object::Param a0) +{ + return mozilla::jni::Method::Call(nullptr, nullptr, a0); +} + constexpr char NativePanZoomController::name[]; constexpr char NativePanZoomController::RequestContentRepaintWrapper_t::name[]; diff --git a/widget/android/GeneratedJNIWrappers.h b/widget/android/GeneratedJNIWrappers.h index 182857ad9944..98edf0c22095 100644 --- a/widget/android/GeneratedJNIWrappers.h +++ b/widget/android/GeneratedJNIWrappers.h @@ -1934,6 +1934,21 @@ public: static mozilla::jni::Object::LocalRef RegisterCompositorWrapper(); +public: + struct updateZoomedView_t { + typedef LayerView Owner; + typedef void ReturnType; + typedef void SetterType; + static constexpr char name[] = "updateZoomedView"; + static constexpr char signature[] = + "(Ljava/nio/ByteBuffer;)V"; + static const bool isStatic = true; + static const bool isMultithreaded = true; + static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT; + }; + + static void updateZoomedView(mozilla::jni::Object::Param); + }; class NativePanZoomController : public mozilla::jni::Class { diff --git a/widget/android/nsAppShell.cpp b/widget/android/nsAppShell.cpp index dd8c20dd1f42..7323a4c90bf3 100644 --- a/widget/android/nsAppShell.cpp +++ b/widget/android/nsAppShell.cpp @@ -393,6 +393,33 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait) break; } + case AndroidGeckoEvent::ZOOMEDVIEW: { + if (!mBrowserApp) + break; + int32_t tabId = curEvent->MetaState(); + const nsTArray& points = curEvent->Points(); + float scaleFactor = (float) curEvent->X(); + nsRefPtr javaBuffer = curEvent->ByteBuffer(); + const auto& mBuffer = jni::Object::Ref::From(javaBuffer->GetObject()); + + nsCOMPtr domWindow; + nsCOMPtr tab; + mBrowserApp->GetBrowserTab(tabId, getter_AddRefs(tab)); + if (!tab) { + NS_ERROR("Can't find tab!"); + break; + } + tab->GetWindow(getter_AddRefs(domWindow)); + if (!domWindow) { + NS_ERROR("Can't find dom window!"); + break; + } + NS_ASSERTION(points.Length() == 2, "ZoomedView event does not have enough coordinates"); + nsIntRect r(points[0].x, points[0].y, points[1].x, points[1].y); + AndroidBridge::Bridge()->CaptureZoomedView(domWindow, r, mBuffer, scaleFactor); + break; + } + case AndroidGeckoEvent::VIEWPORT: case AndroidGeckoEvent::BROADCAST: { if (curEvent->Characters().Length() == 0)