Android App Redirect Blocker

Blocks automatic redirects to apps and shows an overlay with options

当前为 2025-03-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         Android App Redirect Blocker
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Blocks automatic redirects to apps and shows an overlay with options
// @author       You
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    
    // Configuration
    const config = {
        // Time in ms to wait before showing the overlay after detecting a redirect
        redirectDelay: 300,
        // Time in ms to keep the overlay visible (0 for infinite)
        overlayDuration: 5000,
        // Position of the overlay - 'top', 'bottom'
        overlayPosition: 'bottom',
        // Enable logging for debugging
        debug: false,
        // Auto-detect and remember new app schemes
        rememberNewSchemes: true,
        // Whether to block all non-HTTP schemes by default
        blockAllAppSchemes: true,
        // List of known app URL schemes to monitor
        knownSchemes: [
            'fb://',
            'twitter://',
            'instagram://',
            'reddit://',
            'tiktok://',
            'youtube://',
            'whatsapp://',
            'telegram://',
            'intent://',
            'market://',
            'play-audio://',
            'zalo://',
            'linkedin://',
            'snapchat://',
            'spotify://',
            'netflix://',
            'maps://',
            'tel://',
            'sms://',
            'mailto://',
            'comgooglemaps://',
            'waze://',
            'viber://',
            'line://',
            'patreon://',
            'discord://',
            'slack://',
            'googlepay://',
            'upi://'
        ]
    };

    // Main variables
    let lastDetectedApp = '';
    let overlayElement = null;
    let redirectTimeout = null;
    let dismissTimeout = null;
    
    // Inject CSS for the overlay
    const injectStyles = () => {
        const style = document.createElement('style');
        style.textContent = `
            .app-redirect-overlay {
                position: fixed;
                ${config.overlayPosition}: 0;
                left: 0;
                width: 100%;
                background-color: rgba(33, 33, 33, 0.95);
                color: white;
                z-index: 2147483647;
                font-family: Arial, sans-serif;
                padding: 15px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
                transition: transform 0.3s ease, opacity 0.3s ease;
                transform: translateY(${config.overlayPosition === 'top' ? '-100%' : '100%'});
                opacity: 0;
                display: flex;
                flex-direction: column;
                box-sizing: border-box;
            }
            .app-redirect-overlay.visible {
                transform: translateY(0);
                opacity: 1;
            }
            .app-redirect-message {
                font-size: 16px;
                margin-bottom: 15px;
                text-align: center;
            }
            .app-redirect-app-name {
                font-weight: bold;
            }
            .app-redirect-buttons {
                display: flex;
                justify-content: space-around;
            }
            .app-redirect-button {
                background-color: #4285f4;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 10px 15px;
                font-size: 14px;
                font-weight: bold;
                cursor: pointer;
                flex: 1;
                margin: 0 5px;
                max-width: 150px;
            }
            .app-redirect-stay {
                background-color: #757575;
            }
            .app-redirect-open {
                background-color: #4caf50;
            }
            .app-redirect-close {
                position: absolute;
                right: 10px;
                top: 10px;
                width: 24px;
                height: 24px;
                opacity: 0.7;
                cursor: pointer;
                background: none;
                border: none;
                color: white;
                font-size: 24px;
                line-height: 24px;
                padding: 0;
            }
            .app-redirect-close:hover {
                opacity: 1;
            }
        `;
        document.head.appendChild(style);
    };
    
    // Create the overlay element
    const createOverlay = () => {
        if (overlayElement) return;
        
        overlayElement = document.createElement('div');
        overlayElement.className = 'app-redirect-overlay';
        overlayElement.innerHTML = `
            <button class="app-redirect-close">&times;</button>
            <div class="app-redirect-message">
                This page is trying to open the <span class="app-redirect-app-name"></span>
            </div>
            <div class="app-redirect-buttons">
                <button class="app-redirect-button app-redirect-stay">Stay in Browser</button>
                <button class="app-redirect-button app-redirect-open">Open App</button>
            </div>
        `;
        
        // Add event listeners
        overlayElement.querySelector('.app-redirect-close').addEventListener('click', hideOverlay);
        overlayElement.querySelector('.app-redirect-stay').addEventListener('click', () => {
            hideOverlay();
            lastDetectedApp = ''; // Reset the last detected app
        });
        
        overlayElement.querySelector('.app-redirect-open').addEventListener('click', () => {
            hideOverlay();
            if (lastDetectedApp) {
                // Proceed with the app redirection
                window.location.href = lastDetectedApp;
            }
        });
        
        document.body.appendChild(overlayElement);
    };
    
    // Show the overlay with app name
    const showOverlay = (appName, redirectUrl) => {
        if (!document.body) return;
        
        if (!overlayElement) {
            createOverlay();
        }
        
        // Clear any existing timeout
        if (dismissTimeout) {
            clearTimeout(dismissTimeout);
            dismissTimeout = null;
        }
        
        // Update the app name in the overlay
        const appNameElement = overlayElement.querySelector('.app-redirect-app-name');
        appNameElement.textContent = appName || 'unknown app';
        
        // Store the redirect URL
        lastDetectedApp = redirectUrl;
        
        // Show the overlay
        overlayElement.classList.add('visible');
        
        // Auto-hide after the specified duration (if not 0)
        if (config.overlayDuration > 0) {
            dismissTimeout = setTimeout(hideOverlay, config.overlayDuration);
        }
    };
    
    // Hide the overlay
    const hideOverlay = () => {
        if (overlayElement) {
            overlayElement.classList.remove('visible');
        }
        
        if (dismissTimeout) {
            clearTimeout(dismissTimeout);
            dismissTimeout = null;
        }
    };
    
    // Extract app name from URL
    const getAppNameFromUrl = (url) => {
        // Try to extract app name from different URL formats
        let appName = 'app';
        
        try {
            // For intent:// URLs
            if (url.startsWith('intent://')) {
                const packageMatch = url.match(/package=([^;&#]+)/);
                if (packageMatch && packageMatch[1]) {
                    // Get app name from package name (com.example.app -> app)
                    appName = packageMatch[1].split('.').pop();
                    
                    // If there's an action, use that as additional info
                    const actionMatch = url.match(/action=([^;&#]+)/);
                    if (actionMatch && actionMatch[1]) {
                        const action = actionMatch[1].split('.').pop();
                        if (action && action.toLowerCase() !== 'view' && action.toLowerCase() !== 'main') {
                            appName += ' (' + action + ')';
                        }
                    }
                }
            }
            // For android-app:// URLs
            else if (url.startsWith('android-app://')) {
                const parts = url.split('/');
                if (parts.length >= 3) {
                    appName = parts[2].split('.').pop();
                }
            }
            // For direct scheme URLs (fb://, twitter://, etc.)
            else if (url.includes('://')) {
                appName = url.split('://')[0];
                
                // Try to get more context if available
                const urlParts = url.split('://')[1].split('/');
                if (urlParts.length > 1 && urlParts[1] && urlParts[1].length > 0) {
                    const context = urlParts[1];
                    if (context && context.length < 15 && !/^\d+$/.test(context)) {
                        appName += ' ' + context;
                    }
                }
            }
            
            // Clean up and capitalize
            appName = appName.replace(/[^a-zA-Z0-9]/g, ' ').trim();
            appName = appName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
            
            // Handle common URL schemes
            const commonApps = {
                'fb': 'Facebook',
                'twitter': 'Twitter',
                'instagram': 'Instagram',
                'reddit': 'Reddit',
                'tiktok': 'TikTok',
                'youtube': 'YouTube',
                'whatsapp': 'WhatsApp',
                'telegram': 'Telegram',
                'market': 'Play Store',
                'play': 'Google Play',
                'zalo': 'Zalo',
                'linkedin': 'LinkedIn',
                'snapchat': 'Snapchat',
                'spotify': 'Spotify',
                'netflix': 'Netflix',
                'maps': 'Maps',
                'tel': 'Phone Call',
                'sms': 'SMS Message',
                'mailto': 'Email App',
                'intent': 'Android App',
                'comgooglemaps': 'Google Maps',
                'waze': 'Waze'
            };
            
            // Check for common apps
            const lowerAppName = appName.toLowerCase().split(' ')[0];
            if (commonApps[lowerAppName]) {
                appName = commonApps[lowerAppName];
            }
        } catch (e) {
            console.error('Error parsing app name:', e);
        }
        
        return appName;
    };
    
    // Function to detect and intercept app redirects
    const detectRedirect = (url) => {
        // Check if this is a URL we want to intercept
        
        // Handle known schemes from our list
        const isKnownAppUrl = config.knownSchemes.some(scheme => url.startsWith(scheme));
        
        // Handle intent and android-app specific URLs
        const isIntentUrl = url.includes('intent://') || url.includes('android-app://');
        
        // Handle any URL scheme that isn't http/https (likely an app)
        const urlObj = (() => {
            try {
                return new URL(url);
            } catch(e) {
                return null;
            }
        })();
        
        const isCustomScheme = urlObj && 
                          !['http:', 'https:', 'ftp:', 'file:', 'data:', 'javascript:'].includes(urlObj.protocol.toLowerCase()) && 
                          urlObj.protocol !== ':';
        
        if (isKnownAppUrl || isIntentUrl || isCustomScheme) {
            // Prevent the default behavior and show our overlay instead
            const appName = getAppNameFromUrl(url);
            
            // Use a small delay before showing the overlay to allow for quick redirects
            if (redirectTimeout) {
                clearTimeout(redirectTimeout);
            }
            
            redirectTimeout = setTimeout(() => {
                showOverlay(appName, url);
            }, config.redirectDelay);
            
            return true;
        }
        
        return false;
    };
    
    // Intercept location changes
    const originalAssign = window.location.assign;
    window.location.assign = function(url) {
        if (detectRedirect(url)) {
            return;
        }
        return originalAssign.apply(this, arguments);
    };
    
    const originalReplace = window.location.replace;
    window.location.replace = function(url) {
        if (detectRedirect(url)) {
            return;
        }
        return originalReplace.apply(this, arguments);
    };
    
    // Override the href property
    let locationHrefDescriptor = Object.getOwnPropertyDescriptor(window.location, 'href');
    if (locationHrefDescriptor && locationHrefDescriptor.configurable) {
        Object.defineProperty(window.location, 'href', {
            set: function(url) {
                if (detectRedirect(url)) {
                    return url;
                }
                return locationHrefDescriptor.set.call(this, url);
            },
            get: locationHrefDescriptor.get
        });
    }
    
    // Intercept window.open
    const originalWindowOpen = window.open;
    window.open = function(url, ...args) {
        if (url && typeof url === 'string' && detectRedirect(url)) {
            return null;
        }
        return originalWindowOpen.call(this, url, ...args);
    };
    
    // Listen for clicks on links
    document.addEventListener('click', function(e) {
        // Check if the click was on a link
        let element = e.target;
        while (element && element !== document.body) {
            if (element.tagName === 'A' && element.href) {
                const href = element.href;
                if (config.knownSchemes.some(scheme => href.startsWith(scheme)) || 
                    href.includes('intent://') || 
                    href.includes('android-app://')) {
                    
                    e.preventDefault();
                    e.stopPropagation();
                    
                    // Show the overlay
                    const appName = getAppNameFromUrl(href);
                    showOverlay(appName, href);
                    return;
                }
            }
            element = element.parentElement;
        }
    }, true);
    
    // Handle DOM ready
    const onDomReady = () => {
        injectStyles();
    };
    
    // Initialize
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onDomReady);
    } else {
        onDomReady();
    }
    
    // Function to add new schemes to the known list
    const addNewScheme = (url) => {
        try {
            const urlObj = new URL(url);
            const scheme = urlObj.protocol + '//';
            
            // Check if this scheme is already in our list
            if (!config.knownSchemes.includes(scheme) && 
                !['http://', 'https://', 'ftp://', 'file://', 'data://', 'javascript://'].includes(scheme)) {
                
                // Add this new scheme to our list
                config.knownSchemes.push(scheme);
                
                if (config.debug) {
                    console.log('[App Redirect Blocker] Added new scheme:', scheme);
                }
                
                // Store in localStorage for persistence
                try {
                    const storedSchemes = JSON.parse(localStorage.getItem('appRedirectBlocker_schemes') || '[]');
                    if (!storedSchemes.includes(scheme)) {
                        storedSchemes.push(scheme);
                        localStorage.setItem('appRedirectBlocker_schemes', JSON.stringify(storedSchemes));
                    }
                } catch (e) {
                    console.error('[App Redirect Blocker] Error storing scheme:', e);
                }
            }
        } catch (e) {
            if (config.debug) {
                console.error('[App Redirect Blocker] Error adding scheme:', e);
            }
        }
    };
    
    // Load any previously stored schemes
    try {
        const storedSchemes = JSON.parse(localStorage.getItem('appRedirectBlocker_schemes') || '[]');
        for (const scheme of storedSchemes) {
            if (!config.knownSchemes.includes(scheme)) {
                config.knownSchemes.push(scheme);
                if (config.debug) {
                    console.log('[App Redirect Blocker] Loaded stored scheme:', scheme);
                }
            }
        }
    } catch (e) {
        console.error('[App Redirect Blocker] Error loading stored schemes:', e);
    }
    
    // Expose the API to the window object for debugging and configuration
    window.AppRedirectBlocker = {
        showOverlay,
        hideOverlay,
        config,
        addScheme: (scheme) => {
            if (!scheme.endsWith('://')) {
                scheme += '://';
            }
            if (!config.knownSchemes.includes(scheme)) {
                config.knownSchemes.push(scheme);
                return true;
            }
            return false;
        },
        removeScheme: (scheme) => {
            if (!scheme.endsWith('://')) {
                scheme += '://';
            }
            const index = config.knownSchemes.indexOf(scheme);
            if (index !== -1) {
                config.knownSchemes.splice(index, 1);
                return true;
            }
            return false;
        },
        debug: (enable) => {
            config.debug = !!enable;
        }
    };
})();