Bug 1768334 [wpt PR 33981] - [block-in-inline] Fix hit-testing when a block-in-inline...
[gecko.git] / mobile / android / actors / GeckoViewContentChild.jsm
blob8aeba85e12f85ab8c3db63d4207364a88479d2a1
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/. */
5 const { GeckoViewActorChild } = ChromeUtils.import(
6   "resource://gre/modules/GeckoViewActorChild.jsm"
7 );
9 var { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
13 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
15 // This needs to match ScreenLength.java
16 const SCREEN_LENGTH_TYPE_PIXEL = 0;
17 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1;
18 const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2;
19 const SCREEN_LENGTH_DOCUMENT_WIDTH = 3;
20 const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4;
22 // This need to match PanZoomController.java
23 const SCROLL_BEHAVIOR_SMOOTH = 0;
24 const SCROLL_BEHAVIOR_AUTO = 1;
26 const SCREEN_ORIENTATION_PORTRAIT = 0;
27 const SCREEN_ORIENTATION_LANDSCAPE = 1;
29 XPCOMUtils.defineLazyModuleGetters(this, {
30   E10SUtils: "resource://gre/modules/E10SUtils.jsm",
31   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
32   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
33   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
34 });
36 var EXPORTED_SYMBOLS = ["GeckoViewContentChild"];
38 class GeckoViewContentChild extends GeckoViewActorChild {
39   constructor() {
40     super();
41     this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT;
42   }
44   actorCreated() {
45     super.actorCreated();
47     this.pageShow = new Promise(resolve => {
48       this.receivedPageShow = resolve;
49     });
50   }
52   toPixels(aLength, aType) {
53     const { contentWindow } = this;
54     if (aType === SCREEN_LENGTH_TYPE_PIXEL) {
55       return aLength;
56     } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) {
57       return aLength * contentWindow.visualViewport.width;
58     } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) {
59       return aLength * contentWindow.visualViewport.height;
60     } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) {
61       return aLength * contentWindow.document.body.scrollWidth;
62     } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) {
63       return aLength * contentWindow.document.body.scrollHeight;
64     }
66     return aLength;
67   }
69   toScrollBehavior(aBehavior) {
70     const { contentWindow } = this;
71     if (!contentWindow) {
72       return 0;
73     }
74     const { windowUtils } = contentWindow;
75     if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) {
76       return windowUtils.SCROLL_MODE_SMOOTH;
77     } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) {
78       return windowUtils.SCROLL_MODE_INSTANT;
79     }
80     return windowUtils.SCROLL_MODE_SMOOTH;
81   }
83   collectSessionState() {
84     const { docShell, contentWindow } = this;
85     const history = SessionHistory.collect(docShell);
86     let formdata = SessionStoreUtils.collectFormData(contentWindow);
87     let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow);
89     // Save the current document resolution.
90     let zoom = 1;
91     const domWindowUtils = contentWindow.windowUtils;
92     zoom = domWindowUtils.getResolution();
93     scrolldata = scrolldata || {};
94     scrolldata.zoom = {};
95     scrolldata.zoom.resolution = zoom;
97     // Save some data that'll help in adjusting the zoom level
98     // when restoring in a different screen orientation.
99     const displaySize = {};
100     const width = {},
101       height = {};
102     domWindowUtils.getContentViewerSize(width, height);
104     displaySize.width = width.value;
105     displaySize.height = height.value;
107     scrolldata.zoom.displaySize = displaySize;
109     formdata = PrivacyFilter.filterFormData(formdata || {});
111     return { history, formdata, scrolldata };
112   }
114   orientation() {
115     const currentOrientationType = this.contentWindow?.screen.orientation.type;
116     if (!currentOrientationType) {
117       // Unfortunately, we don't know current screen orientation.
118       // Return portrait as default.
119       return SCREEN_ORIENTATION_PORTRAIT;
120     }
121     if (currentOrientationType.startsWith("landscape")) {
122       return SCREEN_ORIENTATION_LANDSCAPE;
123     }
124     return SCREEN_ORIENTATION_PORTRAIT;
125   }
127   receiveMessage(message) {
128     const { name } = message;
129     debug`receiveMessage: ${name}`;
131     switch (name) {
132       case "GeckoView:DOMFullscreenEntered":
133         this.lastOrientation = this.orientation();
134         this.contentWindow?.windowUtils.handleFullscreenRequests();
135         break;
136       case "GeckoView:DOMFullscreenExited":
137         // During fullscreen, window size is changed. So don't restore viewport size.
138         const restoreViewSize = this.orientation() == this.lastOrientation;
139         this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize);
140         break;
141       case "GeckoView:ZoomToInput": {
142         const { contentWindow } = this;
143         const dwu = contentWindow.windowUtils;
145         const zoomToFocusedInput = function() {
146           if (!dwu.flushApzRepaints()) {
147             dwu.zoomToFocusedInput();
148             return;
149           }
150           Services.obs.addObserver(function apzFlushDone() {
151             Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
152             dwu.zoomToFocusedInput();
153           }, "apz-repaints-flushed");
154         };
156         const { force } = message.data;
158         let gotResize = false;
159         const onResize = function() {
160           gotResize = true;
161           if (dwu.isMozAfterPaintPending) {
162             contentWindow.windowRoot.addEventListener(
163               "MozAfterPaint",
164               () => zoomToFocusedInput(),
165               { capture: true, once: true }
166             );
167           } else {
168             zoomToFocusedInput();
169           }
170         };
172         contentWindow.addEventListener("resize", onResize, { capture: true });
174         // When the keyboard is displayed, we can get one resize event,
175         // multiple resize events, or none at all. Try to handle all these
176         // cases by allowing resizing within a set interval, and still zoom to
177         // input if there is no resize event at the end of the interval.
178         contentWindow.setTimeout(() => {
179           contentWindow.removeEventListener("resize", onResize, {
180             capture: true,
181           });
182           if (!gotResize && force) {
183             onResize();
184           }
185         }, 500);
186         break;
187       }
188       case "RestoreSessionState": {
189         this.restoreSessionState(message);
190         break;
191       }
192       case "RestoreHistoryAndNavigate": {
193         const { history, switchId } = message.data;
194         if (history) {
195           SessionHistory.restore(this.docShell, history);
196           const historyIndex = history.requestedIndex - 1;
197           const webNavigation = this.docShell.QueryInterface(
198             Ci.nsIWebNavigation
199           );
201           if (!switchId) {
202             // TODO: Bug 1648158 This won't work for Fission or HistoryInParent.
203             webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry();
204           } else {
205             webNavigation.resumeRedirectedLoad(switchId, historyIndex);
206           }
207         }
208         break;
209       }
210       case "GeckoView:UpdateInitData": {
211         // Provide a hook for native code to detect a transfer.
212         Services.obs.notifyObservers(
213           this.docShell,
214           "geckoview-content-global-transferred"
215         );
216         break;
217       }
218       case "GeckoView:ScrollBy": {
219         const x = {};
220         const y = {};
221         const { contentWindow } = this;
222         const {
223           widthValue,
224           widthType,
225           heightValue,
226           heightType,
227           behavior,
228         } = message.data;
229         contentWindow.windowUtils.getVisualViewportOffset(x, y);
230         contentWindow.windowUtils.scrollToVisual(
231           x.value + this.toPixels(widthValue, widthType),
232           y.value + this.toPixels(heightValue, heightType),
233           contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
234           this.toScrollBehavior(behavior)
235         );
236         break;
237       }
238       case "GeckoView:ScrollTo": {
239         const { contentWindow } = this;
240         const {
241           widthValue,
242           widthType,
243           heightValue,
244           heightType,
245           behavior,
246         } = message.data;
247         contentWindow.windowUtils.scrollToVisual(
248           this.toPixels(widthValue, widthType),
249           this.toPixels(heightValue, heightType),
250           contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
251           this.toScrollBehavior(behavior)
252         );
253         break;
254       }
255       case "CollectSessionState": {
256         return this.collectSessionState();
257       }
258     }
260     return null;
261   }
263   async restoreSessionState(message) {
264     // Make sure we showed something before restoring scrolling and form data
265     await this.pageShow;
267     const { contentWindow } = this;
268     const { formdata, scrolldata } = message.data;
270     if (formdata) {
271       Utils.restoreFrameTreeData(contentWindow, formdata, (frame, data) => {
272         // restore() will return false, and thus abort restoration for the
273         // current |frame| and its descendants, if |data.url| is given but
274         // doesn't match the loaded document's URL.
275         return SessionStoreUtils.restoreFormData(frame.document, data);
276       });
277     }
279     if (scrolldata) {
280       Utils.restoreFrameTreeData(contentWindow, scrolldata, (frame, data) => {
281         if (data.scroll) {
282           SessionStoreUtils.restoreScrollPosition(frame, data);
283         }
284       });
285     }
287     if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) {
288       const utils = contentWindow.windowUtils;
289       // Restore zoom level.
290       utils.setRestoreResolution(
291         scrolldata.zoom.resolution,
292         scrolldata.zoom.displaySize.width,
293         scrolldata.zoom.displaySize.height
294       );
295     }
296   }
298   // eslint-disable-next-line complexity
299   handleEvent(aEvent) {
300     debug`handleEvent: ${aEvent.type}`;
302     switch (aEvent.type) {
303       case "pageshow": {
304         this.receivedPageShow();
305         break;
306       }
308       case "mozcaretstatechanged":
309         if (
310           aEvent.reason === "presscaret" ||
311           aEvent.reason === "releasecaret"
312         ) {
313           this.eventDispatcher.sendRequest({
314             type: "GeckoView:PinOnScreen",
315             pinned: aEvent.reason === "presscaret",
316           });
317         }
318         break;
319     }
320   }
323 const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent");