GeckoViewContentChild.sys.mjs (11312B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs"; 6 7 // This needs to match ScreenLength.java 8 const SCREEN_LENGTH_TYPE_PIXEL = 0; 9 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1; 10 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2; 11 const SCREEN_LENGTH_DOCUMENT_WIDTH = 3; 12 const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4; 13 14 // This need to match PanZoomController.java 15 const SCROLL_BEHAVIOR_SMOOTH = 0; 16 const SCROLL_BEHAVIOR_AUTO = 1; 17 18 const SCREEN_ORIENTATION_PORTRAIT = 0; 19 const SCREEN_ORIENTATION_LANDSCAPE = 1; 20 21 const lazy = {}; 22 23 ChromeUtils.defineESModuleGetters(lazy, { 24 PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs", 25 SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", 26 }); 27 28 export class GeckoViewContentChild extends GeckoViewActorChild { 29 constructor() { 30 super(); 31 this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT; 32 } 33 34 actorCreated() { 35 this.pageShow = new Promise(resolve => { 36 this.receivedPageShow = resolve; 37 }); 38 } 39 40 toPixels(aLength, aType) { 41 const { contentWindow } = this; 42 if (aType === SCREEN_LENGTH_TYPE_PIXEL) { 43 return aLength; 44 } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) { 45 return aLength * contentWindow.visualViewport.width; 46 } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) { 47 return aLength * contentWindow.visualViewport.height; 48 } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) { 49 return aLength * contentWindow.document.body.scrollWidth; 50 } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) { 51 return aLength * contentWindow.document.body.scrollHeight; 52 } 53 54 return aLength; 55 } 56 57 toScrollBehavior(aBehavior) { 58 const { contentWindow } = this; 59 if (!contentWindow) { 60 return 0; 61 } 62 const { windowUtils } = contentWindow; 63 if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) { 64 return windowUtils.SCROLL_MODE_SMOOTH; 65 } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) { 66 return windowUtils.SCROLL_MODE_INSTANT; 67 } 68 return windowUtils.SCROLL_MODE_SMOOTH; 69 } 70 71 collectSessionState() { 72 const { docShell, contentWindow } = this; 73 const history = lazy.SessionHistory.collect(docShell); 74 let formdata = SessionStoreUtils.collectFormData(contentWindow); 75 let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow); 76 77 // Save the current document resolution. 78 let zoom = 1; 79 const domWindowUtils = contentWindow.windowUtils; 80 zoom = domWindowUtils.getResolution(); 81 scrolldata = scrolldata || {}; 82 scrolldata.zoom = {}; 83 scrolldata.zoom.resolution = zoom; 84 85 // Save some data that'll help in adjusting the zoom level 86 // when restoring in a different screen orientation. 87 const displaySize = {}; 88 const width = {}, 89 height = {}; 90 domWindowUtils.getDocumentViewerSize(width, height); 91 92 displaySize.width = width.value; 93 displaySize.height = height.value; 94 95 scrolldata.zoom.displaySize = displaySize; 96 97 formdata = lazy.PrivacyFilter.filterFormData(formdata || {}); 98 99 return { history, formdata, scrolldata }; 100 } 101 102 orientation() { 103 const currentOrientationType = this.contentWindow?.screen.orientation.type; 104 if (!currentOrientationType) { 105 // Unfortunately, we don't know current screen orientation. 106 // Return portrait as default. 107 return SCREEN_ORIENTATION_PORTRAIT; 108 } 109 if (currentOrientationType.startsWith("landscape")) { 110 return SCREEN_ORIENTATION_LANDSCAPE; 111 } 112 return SCREEN_ORIENTATION_PORTRAIT; 113 } 114 115 receiveMessage(message) { 116 const { name } = message; 117 debug`receiveMessage: ${name}`; 118 119 switch (name) { 120 case "GeckoView:DOMFullscreenEntered": { 121 const windowUtils = this.contentWindow?.windowUtils; 122 const actor = 123 this.contentWindow?.windowGlobalChild?.getActor("ContentDelegate"); 124 if (!windowUtils) { 125 // If we are not able to enter fullscreen, tell the parent to just 126 // exit. 127 actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {}); 128 break; 129 } 130 this.lastOrientation = this.orientation(); 131 let remoteFrameBC = message.data.remoteFrameBC; 132 if (remoteFrameBC) { 133 let remoteFrame = remoteFrameBC.embedderElement; 134 if (!remoteFrame) { 135 // This could happen when the page navigate away and trigger a 136 // process switching during fullscreen transition, tell the parent 137 // to just exit. 138 actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {}); 139 break; 140 } 141 142 windowUtils.remoteFrameFullscreenChanged(remoteFrame); 143 break; 144 } 145 146 if ( 147 !windowUtils.handleFullscreenRequests() && 148 !this.contentWindow?.document.fullscreenElement 149 ) { 150 // If we don't actually have any pending fullscreen request 151 // to handle, neither we have been in fullscreen, tell the 152 // parent to just exit. 153 actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {}); 154 } 155 break; 156 } 157 case "GeckoView:DOMFullscreenExited": { 158 // During fullscreen, window size is changed. So don't restore viewport size. 159 const restoreViewSize = this.orientation() == this.lastOrientation; 160 this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize); 161 break; 162 } 163 case "GeckoView:ZoomToInput": { 164 const { contentWindow } = this; 165 const dwu = contentWindow.windowUtils; 166 167 const zoomToFocusedInput = function () { 168 if (!dwu.flushApzRepaints()) { 169 dwu.zoomToFocusedInput(); 170 return; 171 } 172 Services.obs.addObserver(function apzFlushDone() { 173 Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed"); 174 dwu.zoomToFocusedInput(); 175 }, "apz-repaints-flushed"); 176 }; 177 178 zoomToFocusedInput(); 179 break; 180 } 181 case "RestoreSessionState": { 182 this.restoreSessionState(message); 183 break; 184 } 185 case "RestoreHistoryAndNavigate": { 186 const { history, switchId } = message.data; 187 if (history) { 188 lazy.SessionHistory.restore(this.docShell, history); 189 const historyIndex = history.requestedIndex - 1; 190 const webNavigation = this.docShell.QueryInterface( 191 Ci.nsIWebNavigation 192 ); 193 194 if (!switchId) { 195 // TODO: Bug 1648158 This won't work for Fission or HistoryInParent. 196 webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry(); 197 } else { 198 webNavigation.resumeRedirectedLoad(switchId, historyIndex); 199 } 200 } 201 break; 202 } 203 case "GeckoView:UpdateInitData": { 204 // Provide a hook for native code to detect a transfer. 205 Services.obs.notifyObservers( 206 this.docShell, 207 "geckoview-content-global-transferred" 208 ); 209 break; 210 } 211 case "GeckoView:ScrollBy": { 212 const x = {}; 213 const y = {}; 214 const { contentWindow } = this; 215 const { widthValue, widthType, heightValue, heightType, behavior } = 216 message.data; 217 contentWindow.windowUtils.getVisualViewportOffset(x, y); 218 contentWindow.windowUtils.scrollToVisual( 219 x.value + this.toPixels(widthValue, widthType), 220 y.value + this.toPixels(heightValue, heightType), 221 contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD, 222 this.toScrollBehavior(behavior) 223 ); 224 break; 225 } 226 case "GeckoView:ScrollTo": { 227 const { contentWindow } = this; 228 const { widthValue, widthType, heightValue, heightType, behavior } = 229 message.data; 230 contentWindow.windowUtils.scrollToVisual( 231 this.toPixels(widthValue, widthType), 232 this.toPixels(heightValue, heightType), 233 contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD, 234 this.toScrollBehavior(behavior) 235 ); 236 break; 237 } 238 case "CollectSessionState": { 239 return this.collectSessionState(); 240 } 241 case "ContainsFormData": { 242 return this.containsFormData(); 243 } 244 } 245 246 return null; 247 } 248 249 async containsFormData() { 250 const { contentWindow } = this; 251 let formdata = SessionStoreUtils.collectFormData(contentWindow); 252 formdata = lazy.PrivacyFilter.filterFormData(formdata || {}); 253 if (formdata) { 254 return true; 255 } 256 return false; 257 } 258 259 async restoreSessionState(message) { 260 // Make sure we showed something before restoring scrolling and form data 261 await this.pageShow; 262 263 const { contentWindow } = this; 264 const { formdata, scrolldata } = message.data; 265 266 /** 267 * Restores frame tree |data|, starting at the given root |frame|. As the 268 * function recurses into descendant frames it will call cb(frame, data) for 269 * each frame it encounters, starting with the given root. 270 */ 271 function restoreFrameTreeData(frame, data, cb) { 272 // Restore data for the root frame. 273 // The callback can abort by returning false. 274 if (cb(frame, data) === false) { 275 return; 276 } 277 278 if (!data.hasOwnProperty("children")) { 279 return; 280 } 281 282 // Recurse into child frames. 283 SessionStoreUtils.forEachNonDynamicChildFrame( 284 frame, 285 (subframe, index) => { 286 if (data.children[index]) { 287 restoreFrameTreeData(subframe, data.children[index], cb); 288 } 289 } 290 ); 291 } 292 293 if (formdata) { 294 restoreFrameTreeData(contentWindow, formdata, (frame, data) => { 295 // restore() will return false, and thus abort restoration for the 296 // current |frame| and its descendants, if |data.url| is given but 297 // doesn't match the loaded document's URL. 298 return SessionStoreUtils.restoreFormData(frame.document, data); 299 }); 300 } 301 302 if (scrolldata) { 303 restoreFrameTreeData(contentWindow, scrolldata, (frame, data) => { 304 if (data.scroll) { 305 SessionStoreUtils.restoreScrollPosition(frame, data); 306 } 307 }); 308 } 309 310 if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) { 311 const utils = contentWindow.windowUtils; 312 // Restore zoom level. 313 utils.setRestoreResolution( 314 scrolldata.zoom.resolution, 315 scrolldata.zoom.displaySize.width, 316 scrolldata.zoom.displaySize.height 317 ); 318 } 319 } 320 321 // eslint-disable-next-line complexity 322 handleEvent(aEvent) { 323 debug`handleEvent: ${aEvent.type}`; 324 325 switch (aEvent.type) { 326 case "pageshow": { 327 this.receivedPageShow(); 328 break; 329 } 330 331 case "mozcaretstatechanged": 332 if ( 333 aEvent.reason === "presscaret" || 334 aEvent.reason === "releasecaret" 335 ) { 336 this.sendAsyncMessage("GeckoView:PinOnScreen", { 337 pinned: aEvent.reason === "presscaret", 338 }); 339 } 340 break; 341 } 342 } 343 } 344 345 const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent");