// ==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">×</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;
}
};
})();