Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions apps/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ tauri-plugin-updater = { workspace = true, optional = true }

[target.'cfg(windows)'.dependencies]
webview2-com.workspace = true
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
] }
windows-core.workspace = true

[features]
Expand Down
92 changes: 81 additions & 11 deletions apps/app/src/api/ads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub modal_shown: bool,
pub occluded: bool,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
Expand Down Expand Up @@ -60,8 +61,8 @@ fn configure_ads_cookie_settings(
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
) {
use webview2_com::Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
ICoreWebView2_13, ICoreWebView2Profile3,
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
ICoreWebView2Profile3,
};
use windows_core::Interface;

Expand Down Expand Up @@ -119,7 +120,55 @@ fn set_webview_visible_for_window<R: Runtime>(
.and_then(|window| window.is_minimized().ok())
.unwrap_or(false);

set_webview_visible(webview, visible && !is_minimized);
let is_occluded = app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| state.occluded)
.unwrap_or(false);

set_webview_visible(webview, visible && !is_minimized && !is_occluded);
}

#[cfg(windows)]
fn compute_ads_webview_occlusion<R: Runtime>(
app: &tauri::AppHandle<R>,
) -> Option<bool> {
let main_window = app.get_window("main")?;
let webviews = app.webviews();
let webview = webviews.get("ads-window")?;
let position = webview.position().ok()?;
let size = webview.size().ok()?;
let hwnd = main_window.hwnd().ok()?;

Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
hwnd,
position.x,
position.y,
size.width,
size.height,
))
}

#[cfg(windows)]
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
let Some(occluded) = compute_ads_webview_occlusion(app) else {
return;
};

let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;

if state.occluded == occluded {
return;
}

state.occluded = occluded;
let visible = state.shown && !state.modal_shown;
drop(state);

if let Some(webview) = app.webviews().get("ads-window") {
set_webview_visible_for_window(app, webview, visible);
}
}

fn sync_webview_visibility_for_main_window<R: Runtime>(
Expand All @@ -140,7 +189,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
false
} else {
match app.state::<RwLock<AdsState>>().try_read() {
Ok(state) => state.shown && !state.modal_shown,
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
Err(_) => false,
}
};
Expand Down Expand Up @@ -173,20 +222,28 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app.manage(RwLock::new(AdsState {
shown: true,
modal_shown: false,
occluded: false,
last_click: None,
malicious_origins: HashSet::new(),
}));

// We refresh the ads window every 5 minutes to mitigate memory leak issues.
// While this loop doesn't include explicit checks to see if the window is still
// visible when we refresh, the Aditude wrapper will not make any ad requests
// unless Chromium reports the page as visible. The refresh does not reset the
// visibility state.
// We refresh the ads window periodically to mitigate memory leak issues.
// Skip refreshes when app state has hidden the ads WebView. The refresh does
// not reset the visibility state.
let refresh_app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
let should_refresh = refresh_app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| {
state.shown && !state.modal_shown && !state.occluded
})
.unwrap_or(false);

if should_refresh
&& let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
Expand Down Expand Up @@ -224,6 +281,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
});
}

#[cfg(windows)]
{
let app_handle = app.clone();

tauri::async_runtime::spawn(async move {
loop {
sync_ads_occlusion(&app_handle).await;

tokio::time::sleep(Duration::from_millis(200)).await;
}
});
}

Ok(())
})
.invoke_handler(tauri::generate_handler![
Expand Down
193 changes: 193 additions & 0 deletions apps/app/src/api/ads_occlusion_windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use windows::Win32::Foundation::{HWND, POINT, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::WindowsAndMessaging::{
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
};

const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;

pub fn is_ads_webview_occluded(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> bool {
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
else {
return false;
};

if is_empty_rect(&ad_rect) {
return false;
}

let ad_area = rect_area(&ad_rect);
if ad_area == 0 {
return false;
}

let mut occluded_area = 0u64;
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
let app_process_id = std::process::id();
let mut hwnd = match unsafe { GetTopWindow(None) } {
Ok(hwnd) => hwnd,
Err(_) => return false,
};

while !hwnd.is_invalid() {
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };

if window_root == app_root {
return false;
}

if window_process_id(hwnd) == Some(app_process_id) {
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
continue;
}

if window_counts_as_occluder(hwnd)
&& let Some(occluder_rect) = window_rect(hwnd)
&& let Some(intersection) =
intersect_rects(&ad_rect, &occluder_rect)
{
occluded_area =
occluded_area.saturating_add(rect_area(&intersection));

if (occluded_area as f64 / ad_area as f64)
>= OCCLUDED_AREA_THRESHOLD
{
return true;
}
}

hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
}

false
}

fn ad_rect_in_screen(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<RECT> {
let mut origin = POINT { x: 0, y: 0 };

if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
return None;
}

let left = origin.x.saturating_add(x);
let top = origin.y.saturating_add(y);
let right = left.saturating_add(width as i32);
let bottom = top.saturating_add(height as i32);

Some(RECT {
left,
top,
right,
bottom,
})
}

fn window_counts_as_occluder(hwnd: HWND) -> bool {
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
return false;
}

if unsafe { IsIconic(hwnd).as_bool() } {
return false;
}

if is_dwm_cloaked(hwnd) {
return false;
}

true
}

fn window_process_id(hwnd: HWND) -> Option<u32> {
let mut process_id = 0u32;

unsafe {
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
}

(process_id != 0).then_some(process_id)
}

fn is_dwm_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;

unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
&mut cloaked as *mut u32 as *mut _,
std::mem::size_of::<u32>() as u32,
)
}
.is_ok()
&& cloaked != 0
}

fn window_rect(hwnd: HWND) -> Option<RECT> {
let mut rect = RECT::default();

if unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
&mut rect as *mut RECT as *mut _,
std::mem::size_of::<RECT>() as u32,
)
}
.is_err()
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
{
return None;
}

if is_empty_rect(&rect) {
return None;
}

Some(rect)
}

fn is_empty_rect(rect: &RECT) -> bool {
rect.right <= rect.left || rect.bottom <= rect.top
}

fn rect_area(rect: &RECT) -> u64 {
if is_empty_rect(rect) {
return 0;
}

(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
}

fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
let rect = RECT {
left: a.left.max(b.left),
top: a.top.max(b.top),
right: a.right.min(b.right),
bottom: a.bottom.min(b.bottom),
};

(!is_empty_rect(&rect)).then_some(rect)
}
2 changes: 2 additions & 0 deletions apps/app/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub mod tags;
pub mod utils;

pub mod ads;
#[cfg(windows)]
mod ads_occlusion_windows;
pub mod cache;
pub mod files;
pub mod friends;
Expand Down
Loading