summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/src/init/user_activity.rs
blob: dbafeca4e8646d3e2a017821ecdd1ab235629fd3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::ffi::CStr;
use std::os::raw::c_char;
use std::sync::{
    atomic::{AtomicBool, Ordering},
    RwLock,
};
use std::time::{Duration, Instant};

use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
use xpcom::{
    interfaces::{nsIObserverService, nsISupports},
    RefPtr,
};

// Partially cargo-culted from UploadPrefObserver.
#[xpcom(implement(nsIObserver), atomic)]
pub(crate) struct UserActivityObserver {
    last_edge: RwLock<Instant>,
    was_active: AtomicBool,
}

/// Listens to Firefox Desktop's `user-interaction-(in)active` topics,
/// debouncing them before calling into the Glean SDK Client Activity API.
/// See
/// [the docs](https://firefox-source-docs.mozilla.org/toolkit/components/glean/builtin_pings.html)
/// for more info.
#[allow(non_snake_case)]
impl UserActivityObserver {
    pub(crate) fn begin_observing() -> Result<(), nsresult> {
        // First and foremost, even if we can't get the ObserverService,
        // init always means client activity.
        glean::handle_client_active();

        // SAFETY: Everything here is self-contained.
        //
        // * We allocate the activity observer, created by the xpcom macro
        // * We create cstr from a static string.
        // * We control all input to `AddObserver`
        unsafe {
            let activity_obs = Self::allocate(InitUserActivityObserver {
                last_edge: RwLock::new(Instant::now()),
                was_active: AtomicBool::new(false),
            });
            let obs_service: RefPtr<nsIObserverService> =
                xpcom::components::Observer::service().map_err(|_| NS_ERROR_FAILURE)?;
            let rv = obs_service.AddObserver(
                activity_obs.coerce(),
                cstr!("user-interaction-active").as_ptr(),
                false,
            );
            if !rv.succeeded() {
                return Err(rv);
            }
            let rv = obs_service.AddObserver(
                activity_obs.coerce(),
                cstr!("user-interaction-inactive").as_ptr(),
                false,
            );
            if !rv.succeeded() {
                return Err(rv);
            }
        }
        Ok(())
    }

    unsafe fn Observe(
        &self,
        _subject: *const nsISupports,
        topic: *const c_char,
        _data: *const u16,
    ) -> nserror::nsresult {
        match CStr::from_ptr(topic).to_str() {
            Ok("user-interaction-active") => self.handle_active(),
            Ok("user-interaction-inactive") => self.handle_inactive(),
            _ => NS_OK,
        }
    }

    fn handle_active(&self) -> nserror::nsresult {
        let was_active = self.was_active.swap(true, Ordering::SeqCst);
        if !was_active {
            let inactivity = self
                .last_edge
                .read()
                .expect("Edge lock poisoned.")
                .elapsed();
            // We only care after a certain period of inactivity (default 20min).
            let limit = static_prefs::pref!("telemetry.fog.test.inactivity_limit");
            if inactivity >= Duration::from_secs(limit.into()) {
                log::info!(
                    "User triggers core activity after {}s!",
                    inactivity.as_secs()
                );
                glean::handle_client_active();
            }
            let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
            *edge = Instant::now();
        }
        NS_OK
    }

    fn handle_inactive(&self) -> nserror::nsresult {
        let was_active = self.was_active.swap(false, Ordering::SeqCst);
        // This is actually always so. Inactivity is only notified once.
        if was_active {
            let activity = self
                .last_edge
                .read()
                .expect("Edge lock poisoned.")
                .elapsed();
            // We only care after a certain period of activity (default 2min).
            let limit = static_prefs::pref!("telemetry.fog.test.activity_limit");
            if activity >= Duration::from_secs(limit.into()) {
                log::info!(
                    "User triggers core inactivity after {}s!",
                    activity.as_secs()
                );
                glean::handle_client_inactive();
            }
            let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
            *edge = Instant::now();
        }
        NS_OK
    }
}