/** * Safe Area Inset Injection for Android WebView * * This script injects safe area inset values into CSS environment variables * when running in Android WebView, since Android doesn't natively support * CSS env(safe-area-inset-*) variables like iOS does. */ // Check if we're running in Android WebView with Capacitor const isAndroidWebView = () => { // Check if we're on iOS - if so, skip this script entirely const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); if (isIOS) { return false; } // Check if we're on Android const isAndroid = /Android/.test(navigator.userAgent); // Check if we have Capacitor (required for Android WebView) const hasCapacitor = window.Capacitor !== undefined; // Only run on Android with Capacitor return isAndroid && hasCapacitor; }; // Wait for Capacitor to be available const waitForCapacitor = () => { return new Promise((resolve) => { if (window.Capacitor) { resolve(window.Capacitor); return; } // Wait for Capacitor to be available const checkCapacitor = () => { if (window.Capacitor) { resolve(window.Capacitor); } else { setTimeout(checkCapacitor, 100); } }; checkCapacitor(); }); }; // Inject safe area inset values into CSS custom properties const injectSafeAreaInsets = async () => { try { // Wait for Capacitor to be available const Capacitor = await waitForCapacitor(); // Try to get safe area insets using StatusBar plugin (which is already available) let top = 0, bottom = 0, left = 0, right = 0; try { // Use StatusBar plugin to get status bar height if (Capacitor.Plugins.StatusBar) { const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo(); // Status bar height is typically the top safe area inset top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0; } } catch (error) { // Status bar info not available, will use fallback } // Detect navigation bar and gesture bar heights const detectNavigationBar = () => { const screenHeight = window.screen.height; const screenWidth = window.screen.width; const windowHeight = window.innerHeight; const devicePixelRatio = window.devicePixelRatio || 1; // Calculate navigation bar height let navBarHeight = 0; // Method 1: Direct comparison (most reliable) if (windowHeight < screenHeight) { navBarHeight = screenHeight - windowHeight; } // Method 2: Check for gesture navigation indicators if (navBarHeight === 0) { // Look for common gesture navigation patterns const isTallDevice = screenHeight > 2000; const isModernDevice = screenHeight > 1800; const hasHighDensity = devicePixelRatio >= 2.5; if (isTallDevice && hasHighDensity) { // Modern gesture-based device navBarHeight = 12; // Typical gesture bar height } else if (isModernDevice) { // Modern device with traditional navigation navBarHeight = 48; // Traditional navigation bar height } } // Method 3: Check visual viewport (more accurate for WebView) if (navBarHeight === 0) { if (window.visualViewport) { const visualHeight = window.visualViewport.height; if (visualHeight < windowHeight) { navBarHeight = windowHeight - visualHeight; } } } // Method 4: Device-specific estimation based on screen dimensions if (navBarHeight === 0) { // Common Android navigation bar heights in pixels const commonNavBarHeights = { "1080x2400": 48, // Common 1080p devices "1440x3200": 64, // QHD devices "720x1600": 32, // HD devices }; const resolution = `${screenWidth}x${screenHeight}`; const estimatedHeight = commonNavBarHeights[resolution]; if (estimatedHeight) { navBarHeight = estimatedHeight; } else { // Fallback: estimate based on screen height navBarHeight = screenHeight > 2000 ? 48 : 32; } } return navBarHeight; }; // Get navigation bar height bottom = detectNavigationBar(); // If we still don't have a top value, estimate it if (top === 0) { const screenHeight = window.screen.height; // Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some top = screenHeight > 1920 ? 64 : 48; } // Left/right safe areas are rare on Android left = 0; right = 0; // Create CSS custom properties const style = document.createElement("style"); style.textContent = ` :root { --safe-area-inset-top: ${top}px; --safe-area-inset-bottom: ${bottom}px; --safe-area-inset-left: ${left}px; --safe-area-inset-right: ${right}px; } `; // Inject the style into the document head document.head.appendChild(style); // Also set CSS environment variables if supported if (CSS.supports("env(safe-area-inset-top)")) { document.documentElement.style.setProperty( "--env-safe-area-inset-top", `${top}px`, ); document.documentElement.style.setProperty( "--env-safe-area-inset-bottom", `${bottom}px`, ); document.documentElement.style.setProperty( "--env-safe-area-inset-left", `${left}px`, ); document.documentElement.style.setProperty( "--env-safe-area-inset-right", `${right}px`, ); } } catch (error) { // Error injecting safe area insets, will use fallback values } }; // Initialize when DOM is ready const initializeSafeArea = () => { // Check if we should run this script at all if (!isAndroidWebView()) { return; } // Add a small delay to ensure WebView is fully initialized setTimeout(() => { injectSafeAreaInsets(); }, 100); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeSafeArea); } else { initializeSafeArea(); } // Re-inject on orientation change (only on Android) window.addEventListener("orientationchange", () => { if (isAndroidWebView()) { setTimeout(() => injectSafeAreaInsets(), 100); } }); // Re-inject on resize (only on Android) window.addEventListener("resize", () => { if (isAndroidWebView()) { setTimeout(() => injectSafeAreaInsets(), 100); } }); // Export for use in other modules export { injectSafeAreaInsets, isAndroidWebView };