Skip to main content

script/dom/geolocation/
geolocation.rs

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 https://mozilla.org/MPL/2.0/. */
4use std::cell::Cell;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use dom_struct::dom_struct;
8use embedder_traits::{
9    AllowOrDeny, EmbedderMsg, GeolocationError, GeolocationRequestOptions, GeolocationResult,
10    GeolocationWatchId,
11};
12use js::context::JSContext;
13use rustc_hash::{FxHashMap, FxHashSet};
14use script_bindings::callback::{ExceptionHandling, OwnerWindow, RootedCallback, TracedCallback};
15use script_bindings::cell::DomRefCell;
16use script_bindings::codegen::GenericBindings::DocumentBinding::{
17    DocumentMethods, DocumentVisibilityState,
18};
19use script_bindings::codegen::GenericBindings::GeolocationBinding::Geolocation_Binding::GeolocationMethods;
20use script_bindings::codegen::GenericBindings::GeolocationBinding::{
21    PositionCallback, PositionErrorCallback, PositionOptions,
22};
23use script_bindings::codegen::GenericBindings::PermissionStatusBinding::{
24    PermissionName, PermissionState,
25};
26use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
27use script_bindings::domstring::DOMString;
28use script_bindings::reflector::{Reflector, reflect_dom_object};
29use script_bindings::root::DomRoot;
30use servo_base::generic_channel::GenericCallback;
31
32use crate::dom::bindings::codegen::DomTypeHolder::DomTypeHolder;
33use crate::dom::bindings::refcounted::Trusted;
34use crate::dom::bindings::reflector::DomGlobal;
35use crate::dom::bindings::root::MutNullableDom;
36use crate::dom::geolocationposition::GeolocationPosition;
37use crate::dom::geolocationpositionerror::GeolocationPositionError;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::permissions::{
40    AsyncPermissionRequest, descriptor_permission_state, record_permission_request_result,
41    request_permission_to_use_async,
42};
43use crate::event_loop::timers::{OneshotTimerCallback, OneshotTimerHandle};
44
45/// Identifies one in-flight *request a position* algorithm.
46type RequestId = u32;
47
48/// The `PositionOptions` a request was started with.
49#[derive(Clone, JSTraceable, MallocSizeOf)]
50struct RequestOptions {
51    /// <https://www.w3.org/TR/geolocation/#dom-positionoptions-enablehighaccuracy>
52    enable_high_accuracy: bool,
53    /// <https://www.w3.org/TR/geolocation/#dom-positionoptions-timeout>
54    timeout: u32,
55    /// <https://www.w3.org/TR/geolocation/#dom-positionoptions-maximumage>
56    maximum_age: u32,
57}
58
59impl From<&PositionOptions> for RequestOptions {
60    fn from(options: &PositionOptions) -> Self {
61        Self {
62            enable_high_accuracy: options.enableHighAccuracy,
63            timeout: options.timeout,
64            maximum_age: options.maximumAge,
65        }
66    }
67}
68
69impl From<RequestOptions> for GeolocationRequestOptions {
70    fn from(val: RequestOptions) -> Self {
71        GeolocationRequestOptions {
72            high_accuracy: val.enable_high_accuracy,
73        }
74    }
75}
76
77/// The state of a single in-flight call to `getCurrentPosition()` or `watchPosition()`.
78#[derive(JSTraceable, MallocSizeOf)]
79#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
80struct PositionRequest {
81    success_callback: TracedCallback<PositionCallback<DomTypeHolder>>,
82    error_callback: Option<TracedCallback<PositionErrorCallback<DomTypeHolder>>>,
83    options: RequestOptions,
84    /// The `watchId` returned by `watchPosition()`, or `None` for `getCurrentPosition()`.
85    watch_id: Option<u32>,
86    /// The timeout armed for the acquisition currently under way, if any.
87    timeout_handle: Cell<Option<OneshotTimerHandle>>,
88    /// Set once the current acquisition has produced a result, so that a reply racing the timeout
89    /// is ignored.
90    settled: Cell<bool>,
91    /// Whether the embedder has been asked to keep this watch updated.
92    watching: Cell<bool>,
93    /// Set when the request has failed for good, so that a watch is forgotten once its error
94    /// callback has run rather than waiting for updates that will never come.
95    terminated: Cell<bool>,
96}
97
98impl PositionRequest {
99    fn is_watch(&self) -> bool {
100        self.watch_id.is_some()
101    }
102}
103
104/// What a queued geolocation task will report to script.
105enum Completion {
106    Success(Trusted<GeolocationPosition>),
107    Failure(PositionError),
108}
109
110/// The reason a request failed, resolved into a `GeolocationPositionError` once the
111/// geolocation task runs.
112enum PositionError {
113    PermissionDenied(&'static str),
114    PositionUnavailable(&'static str),
115    Timeout,
116    /// An error reported by the embedding layer's geolocation backend.
117    Embedder(GeolocationError),
118}
119
120impl PositionError {
121    fn create(
122        &self,
123        cx: &mut JSContext,
124        global: &GlobalScope,
125    ) -> DomRoot<GeolocationPositionError> {
126        match self {
127            Self::PermissionDenied(message) => {
128                GeolocationPositionError::permission_denied(cx, global, DOMString::from(*message))
129            },
130            Self::PositionUnavailable(message) => GeolocationPositionError::position_unavailable(
131                cx,
132                global,
133                DOMString::from(*message),
134            ),
135            Self::Timeout => GeolocationPositionError::timeout(
136                cx,
137                global,
138                DOMString::from_static("Acquiring the position timed out"),
139            ),
140            Self::Embedder(error) => {
141                GeolocationPositionError::from_embedder_error(cx, global, error)
142            },
143        }
144    }
145}
146
147/// <https://www.w3.org/TR/geolocation/#dfn-epochtimestamp>
148fn now_as_epoch_timestamp() -> u64 {
149    SystemTime::now()
150        .duration_since(UNIX_EPOCH)
151        .map(|duration| duration.as_millis() as u64)
152        .unwrap_or_default()
153}
154
155#[dom_struct]
156pub struct Geolocation {
157    reflector_: Reflector,
158    /// <https://www.w3.org/TR/geolocation/#dfn-watchids>
159    watch_ids: DomRefCell<FxHashSet<u32>>,
160    next_watch_id: Cell<u32>,
161    /// In-flight requests, keyed by the internal identifier.
162    requests: DomRefCell<FxHashMap<RequestId, PositionRequest>>,
163    next_request_id: Cell<RequestId>,
164    /// Requests parked by step 5 of "request a position" because the document is hidden. They
165    /// resume when it becomes visible again.
166    deferred_until_visible: DomRefCell<Vec<RequestId>>,
167    /// <https://www.w3.org/TR/geolocation/#dfn-cachedposition>
168    cached_position: MutNullableDom<GeolocationPosition>,
169}
170
171impl Geolocation {
172    fn new_inherited() -> Self {
173        Geolocation {
174            reflector_: Reflector::new(),
175            watch_ids: Default::default(),
176            next_watch_id: Cell::new(1),
177            requests: Default::default(),
178            next_request_id: Cell::new(1),
179            deferred_until_visible: Default::default(),
180            cached_position: Default::default(),
181        }
182    }
183
184    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<Self> {
185        reflect_dom_object(cx, Box::new(Self::new_inherited()), global)
186    }
187
188    fn register_request(
189        &self,
190        success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
191        error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
192        options: &PositionOptions,
193        watch_id: Option<u32>,
194    ) -> RequestId {
195        let request_id = self.next_request_id.get();
196        self.next_request_id.set(request_id.wrapping_add(1));
197        self.requests.borrow_mut().insert(
198            request_id,
199            PositionRequest {
200                success_callback: success_callback.to_traced(),
201                error_callback: error_callback.map(|callback| callback.to_traced()),
202                options: options.into(),
203                watch_id,
204                timeout_handle: Cell::new(None),
205                settled: Cell::new(false),
206                watching: Cell::new(false),
207                terminated: Cell::new(false),
208            },
209        );
210        request_id
211    }
212
213    /// Forget a request, disarming its timeout and ending any embedder subscription it owns.
214    fn discard_request(&self, request_id: RequestId) {
215        let Some((timeout_handle, watch_id, watching)) =
216            self.requests.borrow().get(&request_id).map(|request| {
217                (
218                    request.timeout_handle.take(),
219                    request.watch_id,
220                    request.watching.get(),
221                )
222            })
223        else {
224            return;
225        };
226        self.requests.borrow_mut().remove(&request_id);
227
228        if let Some(handle) = timeout_handle {
229            self.global().unschedule_callback(handle);
230        }
231        if let (Some(watch_id), true) = (watch_id, watching) {
232            self.stop_embedder_watch(watch_id);
233        }
234    }
235
236    fn disarm_timeout(&self, request_id: RequestId) {
237        let handle = self
238            .requests
239            .borrow()
240            .get(&request_id)
241            .and_then(|request| request.timeout_handle.take());
242        if let Some(handle) = handle {
243            self.global().unschedule_callback(handle);
244        }
245    }
246
247    fn embedder_watch_id(&self, watch_id: u32) -> Option<GeolocationWatchId> {
248        let global = self.global();
249        Some(GeolocationWatchId {
250            webview_id: global.webview_id()?,
251            pipeline_id: global.pipeline_id(),
252            index: watch_id,
253        })
254    }
255
256    fn stop_embedder_watch(&self, watch_id: u32) {
257        let Some(id) = self.embedder_watch_id(watch_id) else {
258            return;
259        };
260        self.global()
261            .send_to_embedder(EmbedderMsg::StopGeolocationWatch(id));
262    }
263
264    /// Stop every ongoing watch. Called when the `Window` is torn down, so that the embedder does
265    /// not keep a location subscription alive for a pipeline that no longer exists.
266    pub(crate) fn stop_all_watches(&self) {
267        self.deferred_until_visible.borrow_mut().clear();
268        self.watch_ids.borrow_mut().clear();
269
270        for request in self.requests.borrow().values() {
271            if let Some(handle) = request.timeout_handle.take() {
272                self.global().unschedule_callback(handle);
273            }
274            if let (Some(watch_id), true) = (request.watch_id, request.watching.get()) {
275                self.stop_embedder_watch(watch_id);
276            }
277        }
278        self.requests.borrow_mut().clear();
279    }
280
281    /// Whether the requesting Document may currently be given a position. Per step 7.5.2 of
282    /// "request a position", updates are exclusively for fully-active visible documents;
283    /// otherwise they are silently dropped.
284    ///
285    /// <https://www.w3.org/TR/geolocation/#dfn-request-a-position>
286    fn document_can_receive_positions(&self) -> bool {
287        let document = self.global().as_window().Document();
288        document.is_fully_active() && document.VisibilityState() == DocumentVisibilityState::Visible
289    }
290
291    /// Queue a task on the geolocation task source that reports `completion` to the callbacks of
292    /// `request_id`. Once reported, a `getCurrentPosition()` request is finished and forgotten,
293    /// while a watch stays registered to receive further updates.
294    ///
295    /// <https://www.w3.org/TR/geolocation/#dfn-call-back-with-error>
296    fn queue_completion(&self, request_id: RequestId, completion: Completion) {
297        let this = Trusted::new(self);
298        self.global()
299            .task_manager()
300            .geolocation_task_source()
301            .queue(task!(geolocation_completion: move |cx| {
302                this.root().report_completion(cx, request_id, completion);
303            }));
304    }
305
306    /// Invoke the callbacks of `request_id` with the result of an acquisition.
307    fn report_completion(&self, cx: &mut JSContext, request_id: RequestId, completion: Completion) {
308        let Some(keep_listening) = self
309            .requests
310            .borrow()
311            .get(&request_id)
312            .map(|request| request.is_watch() && !request.terminated.get())
313        else {
314            return;
315        };
316
317        match completion {
318            Completion::Success(position) => {
319                rooted!(&in(cx) let callback = self
320                    .requests
321                    .borrow()
322                    .get(&request_id)
323                    .map(|request| request.success_callback.clone()));
324                if let Some(ref callback) = *callback {
325                    let position = position.root();
326                    let _ = callback.Call_(cx, self, &position, ExceptionHandling::Report);
327                }
328            },
329            Completion::Failure(error) => {
330                // Step 1 of `call back with error`: if callback is null, return.
331                rooted!(&in(cx) let callback = self
332                    .requests
333                    .borrow()
334                    .get(&request_id)
335                    .and_then(|request| request.error_callback.clone()));
336                if let Some(ref callback) = *callback {
337                    let position_error = error.create(cx, &self.global());
338                    let _ = callback.Call_(cx, self, &position_error, ExceptionHandling::Report);
339                }
340            },
341        }
342
343        if !keep_listening {
344            self.requests.borrow_mut().remove(&request_id);
345        }
346    }
347
348    /// Report an error and terminate the request, whether or not it is a watch.
349    fn terminate_with_error(&self, request_id: RequestId, error: PositionError) {
350        let Some((watch_id, was_watching)) =
351            self.requests.borrow().get(&request_id).map(|request| {
352                request.settled.set(true);
353                request.terminated.set(true);
354                (request.watch_id, request.watching.replace(false))
355            })
356        else {
357            return;
358        };
359        self.disarm_timeout(request_id);
360        if let Some(watch_id) = watch_id {
361            self.watch_ids.borrow_mut().remove(&watch_id);
362            if was_watching {
363                self.stop_embedder_watch(watch_id);
364            }
365        }
366        self.queue_completion(request_id, Completion::Failure(error));
367    }
368
369    /// Finish the acquisition currently under way and report `completion`. A watch stays
370    /// registered, and the next update from the embedder begins a fresh acquisition.
371    fn complete_acquisition(&self, request_id: RequestId, completion: Completion) {
372        self.queue_completion(request_id, completion);
373        if let Some(request) = self.requests.borrow().get(&request_id) &&
374            request.is_watch()
375        {
376            request.settled.set(false);
377        }
378    }
379
380    /// <https://www.w3.org/TR/geolocation/#dfn-request-a-position>
381    fn request_position(
382        &self,
383        success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
384        error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
385        options: &PositionOptions,
386        watch_id: Option<u32>,
387    ) {
388        let request_id = self.register_request(success_callback, error_callback, options, watch_id);
389
390        // Step 1. Let watchIDs be geolocation's [[watchIDs]].
391        // Step 2. Let document be the geolocation's relevant global object's associated Document.
392        let document = self.global().as_window().Document();
393
394        // Step 3. If document is not allowed to use the "geolocation" feature:
395        if !document.allowed_to_use_feature(PermissionName::Geolocation) {
396            // Step 3.1, 3.2 and 3.3.
397            self.terminate_with_error(
398                request_id,
399                PositionError::PermissionDenied("User denied Geolocation"),
400            );
401            return;
402        }
403
404        // Step 4. If geolocation's environment settings object is a non-secure context:
405        if !self.global().is_secure_context() {
406            // Step 4.1, 4.2 and 4.3.
407            self.terminate_with_error(
408                request_id,
409                PositionError::PermissionDenied("Insecure context for Geolocation"),
410            );
411            return;
412        }
413
414        // Step 5. If document's visibility state is "hidden", wait for the page visibility change
415        // steps to run.
416        if document.VisibilityState() == DocumentVisibilityState::Hidden {
417            self.deferred_until_visible.borrow_mut().push(request_id);
418            return;
419        }
420
421        // Step 6 and 7.
422        self.obtain_permission_then_acquire(request_id);
423    }
424
425    /// Step 6 and 7 of <https://www.w3.org/TR/geolocation/#dfn-request-a-position>: obtain
426    /// permission for the "geolocation" descriptor, then acquire a position.
427    fn obtain_permission_then_acquire(&self, request_id: RequestId) {
428        if !self.requests.borrow().contains_key(&request_id) {
429            // `clearWatch()` reached the request while it was parked waiting for the document to
430            // become visible. Asking the user for permission now would be a prompt for nothing.
431            return;
432        }
433
434        // Step 6. Let descriptor be a new PermissionDescriptor whose name is "geolocation".
435        // Step 7. In parallel, set permission to request permission to use descriptor.
436        let global = self.global();
437        let this = Trusted::new(self);
438        let task_source = global
439            .task_manager()
440            .geolocation_task_source()
441            .to_sendable();
442        let callback = GenericCallback::new(move |response: Result<AllowOrDeny, _>| {
443            let this = this.clone();
444            task_source.queue(task!(geolocation_permission_response: move |_cx| {
445                let this = this.root();
446                let state = match response {
447                    Ok(response) => record_permission_request_result(
448                        PermissionName::Geolocation,
449                        &this.global(),
450                        response,
451                    ),
452                    Err(_) => PermissionState::Denied,
453                };
454                this.handle_permission_state(request_id, state);
455            }));
456        })
457        .expect("Could not create callback in script.");
458
459        match request_permission_to_use_async(PermissionName::Geolocation, &global, callback) {
460            AsyncPermissionRequest::Settled(state) => {
461                self.handle_permission_state(request_id, state)
462            },
463            AsyncPermissionRequest::Prompting => {},
464        }
465    }
466
467    fn handle_permission_state(&self, request_id: RequestId, state: PermissionState) {
468        if state == PermissionState::Granted {
469            // Step 7.3. Wait to acquire a position.
470            self.acquire_position(request_id);
471        } else {
472            // Step 7.2. If permission is "denied", remove watchId from watchIDs and call back
473            // with error passing errorCallback and PERMISSION_DENIED.
474            self.terminate_with_error(
475                request_id,
476                PositionError::PermissionDenied("User denied Geolocation"),
477            );
478        }
479    }
480
481    /// <https://www.w3.org/TR/geolocation/#dfn-acquire-a-position>
482    fn acquire_position(&self, request_id: RequestId) {
483        let Some((options, watch_id)) = self
484            .requests
485            .borrow()
486            .get(&request_id)
487            .map(|request| (request.options.clone(), request.watch_id))
488        else {
489            return;
490        };
491
492        // Step 1. If watchId was passed and [[watchIDs]] does not contain watchId, terminate.
493        if let Some(watch_id) = watch_id &&
494            !self.watch_ids.borrow().contains(&watch_id)
495        {
496            self.discard_request(request_id);
497            return;
498        }
499
500        if let Some(request) = self.requests.borrow().get(&request_id) {
501            request.settled.set(false);
502        }
503
504        // Step 5.1 and 5.2. Re-check the permission state now that acquisition is starting; it
505        // may have been revoked while the document was hidden.
506        if descriptor_permission_state(PermissionName::Geolocation, Some(&self.global())) ==
507            PermissionState::Denied
508        {
509            self.terminate_with_error(
510                request_id,
511                PositionError::PermissionDenied("User denied Geolocation"),
512            );
513            return;
514        }
515
516        // Step 2. Let acquisitionTime be a new EpochTimeStamp that represents now.
517        let acquisition_time = now_as_epoch_timestamp();
518
519        // Step 5.3.2 and 5.3.3. Reuse the cached position if it is fresh enough and was acquired
520        // in the same accuracy mode.
521        if options.maximum_age > 0 &&
522            let Some(cached_position) = self.cached_position.get()
523        {
524            let cache_time = acquisition_time.saturating_sub(options.maximum_age as u64);
525            if cached_position.timestamp() > cache_time &&
526                cached_position.is_high_accuracy() == options.enable_high_accuracy
527            {
528                self.settle(request_id);
529                self.complete_acquisition(
530                    request_id,
531                    Completion::Success(Trusted::new(&*cached_position)),
532                );
533                return;
534            }
535        }
536
537        // Step 3 and 5. The timeout bounds the acquisition that follows, so a zero timeout leaves
538        // no window at all. Fail immediately rather than troubling the embedder.
539        if options.timeout == 0 {
540            self.settle(request_id);
541            self.complete_acquisition(request_id, Completion::Failure(PositionError::Timeout));
542            return;
543        }
544
545        // A watch arms its timeout only for the initial fix. Later updates arrive as the device
546        // moves, and re-arming would report a timeout every time it stands still.
547        self.arm_timeout(request_id, options.timeout);
548
549        // Step 5.3.4. Try to acquire position data from the underlying system.
550        let Some(embedder_msg) = self.build_acquisition_message(request_id, watch_id, options)
551        else {
552            self.settle(request_id);
553            self.complete_acquisition(
554                request_id,
555                Completion::Failure(PositionError::PositionUnavailable(
556                    "No geolocation backend is available.",
557                )),
558            );
559            return;
560        };
561        if watch_id.is_some() &&
562            let Some(request) = self.requests.borrow().get(&request_id)
563        {
564            request.watching.set(true);
565        }
566        self.global().send_to_embedder(embedder_msg);
567    }
568
569    /// Ask the embedding layer for a position, either once or as an ongoing subscription.
570    fn build_acquisition_message(
571        &self,
572        request_id: RequestId,
573        watch_id: Option<u32>,
574        options: RequestOptions,
575    ) -> Option<EmbedderMsg> {
576        let global = self.global();
577        let webview_id = global.webview_id()?;
578        let this = Trusted::new(self);
579        let task_source = global
580            .task_manager()
581            .geolocation_task_source()
582            .to_sendable();
583        let callback = GenericCallback::new(move |result: Result<GeolocationResult, _>| {
584            let this = this.clone();
585            let result = result.unwrap_or_else(|error| {
586                warn!("Failed to receive a position from the embedder ({error:?}).");
587                Err(GeolocationError::position_unavailable(
588                    "Failed to communicate with the geolocation backend.",
589                ))
590            });
591            task_source.queue(task!(geolocation_position_acquired: move |cx| {
592                this.root().handle_acquisition_result(cx, request_id, result);
593            }));
594        })
595        .expect("Could not create callback in script.");
596
597        Some(match watch_id {
598            Some(watch_id) => EmbedderMsg::StartGeolocationWatch(
599                self.embedder_watch_id(watch_id)?,
600                options.into(),
601                callback,
602            ),
603            None => EmbedderMsg::RequestGeolocationPosition(webview_id, options.into(), callback),
604        })
605    }
606
607    /// Handle one reply from the embedding layer. A one-shot request is finished by the first
608    /// reply; for a watch, each further reply is a "significant change of geographic position".
609    fn handle_acquisition_result(
610        &self,
611        cx: &mut JSContext,
612        request_id: RequestId,
613        result: GeolocationResult,
614    ) {
615        let Some((options, watch_id, is_watch)) =
616            self.requests.borrow().get(&request_id).map(|request| {
617                (
618                    request.options.clone(),
619                    request.watch_id,
620                    request.is_watch(),
621                )
622            })
623        else {
624            return;
625        };
626
627        // A watch that was cleared while a reply was in flight produces nothing. A watch that
628        // was terminated by an error is left alone: its completion task will remove it once the
629        // error callback has run.
630        if let Some(watch_id) = watch_id &&
631            !self.watch_ids.borrow().contains(&watch_id)
632        {
633            let terminated = self
634                .requests
635                .borrow()
636                .get(&request_id)
637                .is_some_and(|request| request.terminated.get());
638            if !terminated {
639                self.discard_request(request_id);
640            }
641            return;
642        }
643
644        // Step 7.5.2 of `request a position`: drop updates for documents that are not fully
645        // active and visible.
646        if is_watch && !self.document_can_receive_positions() {
647            return;
648        }
649
650        // The timeout already fired, or an earlier reply already settled this acquisition.
651        if !self.settle(request_id) {
652            return;
653        }
654
655        let completion = match result {
656            Ok(data) => {
657                // Step 5.3.6. Set position to a new GeolocationPosition.
658                match GeolocationPosition::from_position_data(
659                    cx,
660                    &self.global(),
661                    &data,
662                    now_as_epoch_timestamp(),
663                    options.enable_high_accuracy,
664                ) {
665                    Some(position) => {
666                        // Step 5.3.6.3. Set [[cachedPosition]] to position.
667                        self.cached_position.set(Some(&position));
668                        Completion::Success(Trusted::new(&*position))
669                    },
670                    None => Completion::Failure(PositionError::PositionUnavailable(
671                        "The geolocation backend reported an unrepresentable position.",
672                    )),
673                }
674            },
675            Err(error) => Completion::Failure(PositionError::Embedder(error)),
676        };
677        self.complete_acquisition(request_id, completion);
678    }
679
680    /// Mark the current acquisition as finished and disarm its timeout. Returns `false` if it was
681    /// already finished, in which case the caller should report nothing.
682    fn settle(&self, request_id: RequestId) -> bool {
683        let settled = self
684            .requests
685            .borrow()
686            .get(&request_id)
687            .map(|request| request.settled.replace(true));
688        match settled {
689            Some(false) => {
690                self.disarm_timeout(request_id);
691                true
692            },
693            _ => false,
694        }
695    }
696
697    fn arm_timeout(&self, request_id: RequestId, timeout: u32) {
698        let callback = OneshotTimerCallback::GeolocationTimeout(GeolocationTimeoutCallback {
699            geolocation: Trusted::new(self),
700            request_id,
701        });
702        let handle = self
703            .global()
704            .schedule_callback(callback, Duration::from_millis(timeout as u64));
705        if let Some(request) = self.requests.borrow().get(&request_id) {
706            request.timeout_handle.set(Some(handle));
707        }
708    }
709
710    fn handle_timeout(&self, request_id: RequestId) {
711        // The handle is consumed by firing, so forget it before settling.
712        if let Some(request) = self.requests.borrow().get(&request_id) {
713            request.timeout_handle.set(None);
714        }
715        if self.settle(request_id) {
716            self.complete_acquisition(request_id, Completion::Failure(PositionError::Timeout));
717        }
718    }
719
720    /// The geolocation part of the page visibility change steps: resume the requests parked by
721    /// step 5 of "request a position".
722    ///
723    /// <https://www.w3.org/TR/geolocation/#dfn-request-a-position>
724    pub(crate) fn handle_visibility_change(&self, visibility_state: DocumentVisibilityState) {
725        if visibility_state != DocumentVisibilityState::Visible {
726            return;
727        }
728        let deferred = std::mem::take(&mut *self.deferred_until_visible.borrow_mut());
729        for request_id in deferred {
730            self.obtain_permission_then_acquire(request_id);
731        }
732    }
733}
734
735impl GeolocationMethods<DomTypeHolder> for Geolocation {
736    /// <https://www.w3.org/TR/geolocation/#dom-geolocation-getcurrentposition>
737    fn GetCurrentPosition(
738        &self,
739        success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
740        error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
741        options: &PositionOptions,
742    ) {
743        // Step 1. If this's relevant global object's associated Document is not fully active:
744        if !self.global().as_window().Document().is_fully_active() {
745            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
746            let request_id = self.register_request(success_callback, error_callback, options, None);
747            self.queue_completion(
748                request_id,
749                Completion::Failure(PositionError::PositionUnavailable(
750                    "Document is not fully active",
751                )),
752            );
753            // Step 1.2 Terminate this algorithm.
754            return;
755        }
756        // Step 2. Request a position passing this, successCallback, errorCallback, and options.
757        self.request_position(success_callback, error_callback, options, None)
758    }
759
760    /// <https://www.w3.org/TR/geolocation/#watchposition-method>
761    fn WatchPosition(
762        &self,
763        success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
764        error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
765        options: &PositionOptions,
766    ) -> i32 {
767        // Step 1. If this's relevant global object's associated Document is not fully active:
768        if !self.global().as_window().Document().is_fully_active() {
769            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
770            let request_id = self.register_request(success_callback, error_callback, options, None);
771            self.queue_completion(
772                request_id,
773                Completion::Failure(PositionError::PositionUnavailable(
774                    "Document is not fully active",
775                )),
776            );
777            // Step 1.2 Return 0.
778            return 0;
779        }
780        // Step 2. Let watchId be an implementation-defined unsigned long that is greater than zero.
781        let watch_id = self.next_watch_id.get();
782        self.next_watch_id.set(watch_id + 1);
783        // Step 3. Append watchId to this's [[watchIDs]].
784        self.watch_ids.borrow_mut().insert(watch_id);
785        // Step 4. Request a position passing this, successCallback, errorCallback, options, and
786        // watchId.
787        self.request_position(success_callback, error_callback, options, Some(watch_id));
788        // Step 5. Return watchId.
789        watch_id as i32
790    }
791
792    /// <https://www.w3.org/TR/geolocation/#clearwatch-method>
793    fn ClearWatch(&self, watch_id: i32) {
794        let Ok(watch_id) = u32::try_from(watch_id) else {
795            return;
796        };
797        self.watch_ids.borrow_mut().remove(&watch_id);
798
799        let request_id = self
800            .requests
801            .borrow()
802            .iter()
803            .find(|(_, request)| request.watch_id == Some(watch_id))
804            .map(|(request_id, _)| *request_id);
805        if let Some(request_id) = request_id {
806            self.discard_request(request_id);
807        }
808    }
809}
810
811impl OwnerWindow<crate::DomTypeHolder> for Geolocation {}
812
813/// Fires when `PositionOptions.timeout` elapses before a position has been acquired.
814#[derive(JSTraceable, MallocSizeOf)]
815pub(crate) struct GeolocationTimeoutCallback {
816    #[ignore_malloc_size_of = "Because it is non-owning"]
817    geolocation: Trusted<Geolocation>,
818    request_id: RequestId,
819}
820
821impl GeolocationTimeoutCallback {
822    pub(crate) fn invoke(self) {
823        self.geolocation.root().handle_timeout(self.request_id);
824    }
825}