Bug 1888861 - Wait for resize events in the parent process document for ZoomToFocusIn...
[gecko.git] / mobile / android / actors / GeckoViewContentChild.sys.mjs
blobf961aba42d59d3c5ad4ca653034de68d63515da6
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 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
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;
14 // This need to match PanZoomController.java
15 const SCROLL_BEHAVIOR_SMOOTH = 0;
16 const SCROLL_BEHAVIOR_AUTO = 1;
18 const SCREEN_ORIENTATION_PORTRAIT = 0;
19 const SCREEN_ORIENTATION_LANDSCAPE = 1;
21 const lazy = {};
23 ChromeUtils.defineESModuleGetters(lazy, {
24   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs",
25   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
26   Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs",
27 });
29 export class GeckoViewContentChild extends GeckoViewActorChild {
30   constructor() {
31     super();
32     this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT;
33   }
35   actorCreated() {
36     super.actorCreated();
38     this.pageShow = new Promise(resolve => {
39       this.receivedPageShow = resolve;
40     });
41   }
43   toPixels(aLength, aType) {
44     const { contentWindow } = this;
45     if (aType === SCREEN_LENGTH_TYPE_PIXEL) {
46       return aLength;
47     } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) {
48       return aLength * contentWindow.visualViewport.width;
49     } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) {
50       return aLength * contentWindow.visualViewport.height;
51     } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) {
52       return aLength * contentWindow.document.body.scrollWidth;
53     } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) {
54       return aLength * contentWindow.document.body.scrollHeight;
55     }
57     return aLength;
58   }
60   toScrollBehavior(aBehavior) {
61     const { contentWindow } = this;
62     if (!contentWindow) {
63       return 0;
64     }
65     const { windowUtils } = contentWindow;
66     if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) {
67       return windowUtils.SCROLL_MODE_SMOOTH;
68     } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) {
69       return windowUtils.SCROLL_MODE_INSTANT;
70     }
71     return windowUtils.SCROLL_MODE_SMOOTH;
72   }
74   collectSessionState() {
75     const { docShell, contentWindow } = this;
76     const history = lazy.SessionHistory.collect(docShell);
77     let formdata = SessionStoreUtils.collectFormData(contentWindow);
78     let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow);
80     // Save the current document resolution.
81     let zoom = 1;
82     const domWindowUtils = contentWindow.windowUtils;
83     zoom = domWindowUtils.getResolution();
84     scrolldata = scrolldata || {};
85     scrolldata.zoom = {};
86     scrolldata.zoom.resolution = zoom;
88     // Save some data that'll help in adjusting the zoom level
89     // when restoring in a different screen orientation.
90     const displaySize = {};
91     const width = {},
92       height = {};
93     domWindowUtils.getDocumentViewerSize(width, height);
95     displaySize.width = width.value;
96     displaySize.height = height.value;
98     scrolldata.zoom.displaySize = displaySize;
100     formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
102     return { history, formdata, scrolldata };
103   }
105   orientation() {
106     const currentOrientationType = this.contentWindow?.screen.orientation.type;
107     if (!currentOrientationType) {
108       // Unfortunately, we don't know current screen orientation.
109       // Return portrait as default.
110       return SCREEN_ORIENTATION_PORTRAIT;
111     }
112     if (currentOrientationType.startsWith("landscape")) {
113       return SCREEN_ORIENTATION_LANDSCAPE;
114     }
115     return SCREEN_ORIENTATION_PORTRAIT;
116   }
118   receiveMessage(message) {
119     const { name } = message;
120     debug`receiveMessage: ${name}`;
122     switch (name) {
123       case "GeckoView:DOMFullscreenEntered":
124         this.lastOrientation = this.orientation();
125         if (
126           !this.contentWindow?.windowUtils.handleFullscreenRequests() &&
127           !this.contentWindow?.document.fullscreenElement
128         ) {
129           // If we don't actually have any pending fullscreen request
130           // to handle, neither we have been in fullscreen, tell the
131           // parent to just exit.
132           const actor =
133             this.contentWindow?.windowGlobalChild?.getActor("ContentDelegate");
134           actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
135         }
136         break;
137       case "GeckoView:DOMFullscreenExited":
138         // During fullscreen, window size is changed. So don't restore viewport size.
139         const restoreViewSize = this.orientation() == this.lastOrientation;
140         this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize);
141         break;
142       case "GeckoView:ZoomToInput": {
143         const { contentWindow } = this;
144         const dwu = contentWindow.windowUtils;
146         const zoomToFocusedInput = function () {
147           if (!dwu.flushApzRepaints()) {
148             dwu.zoomToFocusedInput();
149             return;
150           }
151           Services.obs.addObserver(function apzFlushDone() {
152             Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
153             dwu.zoomToFocusedInput();
154           }, "apz-repaints-flushed");
155         };
157         zoomToFocusedInput();
158         break;
159       }
160       case "RestoreSessionState": {
161         this.restoreSessionState(message);
162         break;
163       }
164       case "RestoreHistoryAndNavigate": {
165         const { history, switchId } = message.data;
166         if (history) {
167           lazy.SessionHistory.restore(this.docShell, history);
168           const historyIndex = history.requestedIndex - 1;
169           const webNavigation = this.docShell.QueryInterface(
170             Ci.nsIWebNavigation
171           );
173           if (!switchId) {
174             // TODO: Bug 1648158 This won't work for Fission or HistoryInParent.
175             webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry();
176           } else {
177             webNavigation.resumeRedirectedLoad(switchId, historyIndex);
178           }
179         }
180         break;
181       }
182       case "GeckoView:UpdateInitData": {
183         // Provide a hook for native code to detect a transfer.
184         Services.obs.notifyObservers(
185           this.docShell,
186           "geckoview-content-global-transferred"
187         );
188         break;
189       }
190       case "GeckoView:ScrollBy": {
191         const x = {};
192         const y = {};
193         const { contentWindow } = this;
194         const { widthValue, widthType, heightValue, heightType, behavior } =
195           message.data;
196         contentWindow.windowUtils.getVisualViewportOffset(x, y);
197         contentWindow.windowUtils.scrollToVisual(
198           x.value + this.toPixels(widthValue, widthType),
199           y.value + this.toPixels(heightValue, heightType),
200           contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
201           this.toScrollBehavior(behavior)
202         );
203         break;
204       }
205       case "GeckoView:ScrollTo": {
206         const { contentWindow } = this;
207         const { widthValue, widthType, heightValue, heightType, behavior } =
208           message.data;
209         contentWindow.windowUtils.scrollToVisual(
210           this.toPixels(widthValue, widthType),
211           this.toPixels(heightValue, heightType),
212           contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
213           this.toScrollBehavior(behavior)
214         );
215         break;
216       }
217       case "CollectSessionState": {
218         return this.collectSessionState();
219       }
220       case "ContainsFormData": {
221         return this.containsFormData();
222       }
223     }
225     return null;
226   }
228   async containsFormData() {
229     const { contentWindow } = this;
230     let formdata = SessionStoreUtils.collectFormData(contentWindow);
231     formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
232     if (formdata) {
233       return true;
234     }
235     return false;
236   }
238   async restoreSessionState(message) {
239     // Make sure we showed something before restoring scrolling and form data
240     await this.pageShow;
242     const { contentWindow } = this;
243     const { formdata, scrolldata } = message.data;
245     if (formdata) {
246       lazy.Utils.restoreFrameTreeData(
247         contentWindow,
248         formdata,
249         (frame, data) => {
250           // restore() will return false, and thus abort restoration for the
251           // current |frame| and its descendants, if |data.url| is given but
252           // doesn't match the loaded document's URL.
253           return SessionStoreUtils.restoreFormData(frame.document, data);
254         }
255       );
256     }
258     if (scrolldata) {
259       lazy.Utils.restoreFrameTreeData(
260         contentWindow,
261         scrolldata,
262         (frame, data) => {
263           if (data.scroll) {
264             SessionStoreUtils.restoreScrollPosition(frame, data);
265           }
266         }
267       );
268     }
270     if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) {
271       const utils = contentWindow.windowUtils;
272       // Restore zoom level.
273       utils.setRestoreResolution(
274         scrolldata.zoom.resolution,
275         scrolldata.zoom.displaySize.width,
276         scrolldata.zoom.displaySize.height
277       );
278     }
279   }
281   // eslint-disable-next-line complexity
282   handleEvent(aEvent) {
283     debug`handleEvent: ${aEvent.type}`;
285     switch (aEvent.type) {
286       case "pageshow": {
287         this.receivedPageShow();
288         break;
289       }
291       case "mozcaretstatechanged":
292         if (
293           aEvent.reason === "presscaret" ||
294           aEvent.reason === "releasecaret"
295         ) {
296           this.eventDispatcher.sendRequest({
297             type: "GeckoView:PinOnScreen",
298             pinned: aEvent.reason === "presscaret",
299           });
300         }
301         break;
302     }
303   }
306 const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent");