¿Cómo gestionar deep linking y esquemas personalizados en un WebView de Android? Gestionar enlaces profundos en un WebView de Android requiere extender WebViewClient, anular shouldOverrideUrlLoading para verificar isForMainFrame, validar esquemas personalizados contra una lista de permitidos y lanzar de forma segura intenciones externas autorizadas para evitar que los esquemas que no son web permanezcan en la canalización de carga del WebView.
La interceptación de enlaces profundos en Android WebView es la práctica programática de anular devoluciones de llamada de navegación dentro de un WebViewClient personalizado para capturar protocolos que no son HTTP, como esquemas URI personalizados y URIs de Intent de Chrome. Al evaluar los metadatos de la solicitud y validar las cargas útiles de destino, la aplicación host ejecuta lanzamientos de Intent nativos mientras evita bloqueos por protocolos no controlados.
| Término | Definición | Entidad relacionada | Rol de intención de búsqueda |
|---|---|---|---|
| WebView | Un componente de vista integrado basado en Chromium que renderiza contenido web dentro de las aplicaciones de Android. | SDK de Android | Informativo / Comercial |
| Esquema de URL personalizado | Un protocolo URI propietario que permite que los enlaces web se dirijan a aplicaciones nativas. | Enrutamiento de enlaces profundos | Técnico / Informativo |
| Web to App | El proceso de transición de usuarios desde páginas web hacia interfaces de aplicaciones nativas. | Enlaces profundos móviles | Informativo |

Por qué los WebViews de Android arrojan errores net ERR UNKNOWN URL SCHEME
Limitaciones del protocolo Chromium: por qué los WebViews reconocen de forma nativa solo los esquemas web estándar
El componente WebView de Android funciona con un motor de renderizado Chromium subyacente configurado principalmente para analizar y mostrar protocolos de recursos web estándar: http://, https://, file://, data://, about: y javascript:.
De forma predeterminada, cuando no se asigna un WebViewClient personalizado, el marco de Android delega las URL no controladas al Administrador de actividades del sistema. Sin embargo, una vez que una aplicación proporciona un WebViewClient personalizado, los esquemas que no son HTTP(S) (como myapp://product/detail/1024, alipay:// o whatsapp://) se enrutan directamente a las devoluciones de llamada de navegación del cliente. Si la aplicación host permite que un esquema personalizado permanezca dentro de la canalización de carga del WebView en lugar de interceptarlo, el motor de Chromium intenta resolver el esquema personalizado como un recurso web ordinario. Debido a que los protocolos no estándar no pueden resolverse frente a DNS o capas de transporte de servidor web, el motor muestra un fallo de navegación de bajo nivel.
La anatomía del fallo net::ERR_UNKNOWN_URL_SCHEME: desde el toque del usuario hasta el fallo de carga del recurso web
La transición de una interacción del usuario a una página de error ocurre en una secuencia rápida y determinista:
- Activador del usuario: Un usuario toca un botón de llamada a la acción o un hipervínculo dentro de una página de aterrizaje H5 integrada.
- Solicitud de navegación: El motor web construye una solicitud de navegación que contiene la cadena URI de destino (por ejemplo,
myapp://promo/summer). - Evaluación de canalización: Si
shouldOverrideUrlLoading()devuelvefalseo no logra interceptar el URI que no es HTTP(S), el motor de Chromium trata la URL como una carga de recursos web estándar. - Envío de error: Al no encontrar un controlador interno para el protocolo, el motor activa
onReceivedError()y reemplaza el contenido de la vista web con la plantilla de error predeterminada de Chromium:net::ERR_UNKNOWN_URL_SCHEME.
Esta página de error detiene el viaje del usuario, impide el seguimiento de conversiones y degrada la estabilidad percibida de la aplicación host.
El riesgo de fallos por ActivityNotFoundException no controlados en ausencia de la aplicación de destino
Cuando los desarrolladores intentan resolver net::ERR_UNKNOWN_URL_SCHEME por primera vez, un error común es analizar la cadena URI y llamar a context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) sin un control de excepciones adecuado.
Si el usuario no tiene instalada la aplicación de destino en su dispositivo, el sistema operativo Android no puede encontrar una Activity elegible para resolver el Intent. En la ejecución estándar de Android, llamar a startActivity() sin un controlador coincidente lanza una android.content.ActivityNotFoundException no controlada. Si esta excepción no se captura dentro de un bloque try-catch estructurado, todo el proceso de la aplicación host termina abruptamente, lo que resulta en un fallo fatal.
Cómo intercepta shouldOverrideUrlLoading los protocolos de enlaces profundos
Evolución de la API: comparando las sobrecargas de WebResourceRequest de la API 24+ con las devoluciones de llamada de cadena heredadas
La devolución de llamada shouldOverrideUrlLoading del WebViewClient de Android permite que las aplicaciones host inspeccionen e intercepten las solicitudes de navegación antes de que el motor web las ejecute. La firma y las capacidades de este método han evolucionado a través de los niveles de la API de Android:
- API 24+ (Android 7.0 y superior): Sustituye a
shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean. La interfazWebResourceRequestofrece metadatos contextuales enriquecidos, incluyendo la URL de destino (request.url), el método de solicitud HTTP (request.method), los encabezados de solicitud y banderas críticas de jerarquía de marcos (request.isForMainFrame,request.hasGesture()). - Sobrecarga heredada (en desuso en la API 24): Sustituye a
shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean. Esta devolución de llamada heredada recibe solo la cadena URL sin formato sin metadatos sobre el marco de solicitud o el estado de interacción del usuario.
De acuerdo con la Referencia de desarrolladores de Android sobre WebViewClient shouldOverrideUrlLoading, las aplicaciones modernas deben implementar la sobrecarga WebResourceRequest. Al admitir versiones heredadas de Android, la devolución de llamada de cadena en desuso debe configurarse de forma conservadora para evitar omisiones de seguridad.
La criticidad de seguridad de request.isForMainFrame: bloqueo de secuestro de submarcos e iframes

De acuerdo con la Guía para desarrolladores de Android sobre WebResourceRequest, la devolución de llamada shouldOverrideUrlLoading puede activarse para submarcos, iframes y redireccionamientos de recursos en segundo plano, así como para navegaciones de documentos de nivel superior.
Hacer cumplir request.isForMainFrame es esencial para la seguridad de la aplicación:
- Aislamiento de límites de marco: La verificación de
request.isForMainFramegarantiza que los submarcos y iframes no puedan activar lanzamientos de aplicaciones externas o intenciones de esquema personalizadas. Si un submarco intenta navegar, las cargas HTTP(S) estándar deben devolverfalsepara continuar con la renderización en marco, mientras que los esquemas que no son HTTP(S) deben devolvertruepara suprimir la ejecución sin lanzar errores. - Distinción de gestos del usuario: Aunque
isForMainFrameconfirma que la solicitud se aplica al documento principal, no demuestra la activación del usuario. Si su arquitectura requiere verificación de gestos del usuario, evalúerequest.hasGesture()o fuerce modales de confirmación a nivel de aplicación, teniendo en cuenta que la documentación de Android destaca ciertos flujos de usuario válidos dondehasGesture()puede devolver falso.
Diferenciación de la navegación web normal de los traspasos de enlaces profundos: cuándo devolver false vs true
El valor booleano de retorno de shouldOverrideUrlLoading controla el ciclo de vida de renderizado del WebView:
- Devolver
false: Informa al marco de Android que la aplicación host no ha gestionado la URL. ElWebViewcontinúa la navegación web estándar, cargando la URL dentro de su contenedor de Chromium interno. Este valor de retorno es obligatorio para las páginas web estándarhttp://yhttps://que deben mostrarse dentro del navegador integrado. - Devolver
true: Informa al marco que la aplicación host ha interceptado y gestionado la URL. ElWebViewcancela inmediatamente su proceso de carga interno. Este valor de retorno es obligatorio al lanzar unIntentexterno, al enrutar a una actividad nativa o al suprimir un esquema no autorizado.
Gestión de App Links verificados de la aplicación host dentro del contenedor integrado
Cuando un enlace dentro del WebView apunta al dominio HTTPS verificado de la propia aplicación host (como https://app.example.com/product/1024), ejecutar la navegación predeterminada hace que el WebView cargue la versión web de la página del producto.
Para ofrecer una experiencia nativa, el WebViewClient debe inspeccionar las URL HTTPS entrantes. Si el host coincide con el dominio asociado de la aplicación, el cliente intercepta la solicitud, valida la ruta y los parámetros de consulta contra un validador de rutas interno (AppLinkRouteValidator) y envía los datos directamente al enrutador nativo (internalRouter(validatedRoute)), devolviendo true para cancelar la renderización web redundante.
Estructuración de controladores de Intent seguros para esquemas personalizados y URIs de Intent
Disección de la sintaxis URI de Intent de Chrome: esquemas, paquetes y S.browser_fallback_url
En Android, las páginas web utilizan con frecuencia las URIs de Intent de Chrome para estructurar enlaces profundos nativos con mecanismos de respaldo de navegador integrados. Un Intent URI utiliza el esquema intent:// y codifica parámetros de enrutamiento, destinos de paquete y URL de respaldo en una cadena serializada:
intent://product/detail/1024#Intent;scheme=myapp;package=com.example.app;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end
Tal como se documenta en la Guía de Chrome para desarrolladores sobre URIs de Intent de Android, el URI se divide en componentes distintos:
- Host y ruta (
product/detail/1024): Define el destino de la aplicación interna. scheme=myapp: Especifica el protocolo personalizado subyacente.package=com.example.app: Restringe la resolución de intenciones exclusivamente al paquete de Android especificado.S.browser_fallback_url=...: Un extra de cadena opcional que define una URL web o de tienda explícita para cargar si la aplicación de destino no está instalada.
El peligro de ejecutar Intents analizados sin procesar: mitigación de vulnerabilidades de inyección de Intent
Un antipatrón de implementación común es analizar un URI entrante con Intent.parseUri(url, Intent.URI_INTENT_SCHEME) y pasar el Intent resultante directamente a context.startActivity(intent).
De acuerdo con la Orientación de la Guía de pruebas de seguridad de aplicaciones móviles de OWASP sobre enlaces profundos inseguros, la ejecución de intenciones analizadas sin procesar expone a la aplicación a ataques de Inyección de Intent. Un atacante que controle la página web podría crear un URI de Intent que contenga destinos de componentes explícitos maliciosos (component=com.example.app/.InternalAdminActivity), acciones confidenciales o extras de intención privilegiados (S.is_admin=true). El WebView deserializaría estos parámetros y lanzaría actividades no exportadas o internas, eludiendo las barreras de autenticación.

Reconstrucción segura de Intent: eliminación de componentes y restricción de acciones a ACTION_VIEW
Para eliminar las vulnerabilidades de inyección de Intent, las aplicaciones host nunca deben lanzar un Intent deserializado sin procesar directamente. En su lugar, el WebViewClient debe analizar el URI, extraer solo la carga útil de datos verificada y reconstruir un Intent limpio y dedicado:
- Extraer datos: Analice el URI y extraiga el paquete de destino verificado y el URI de datos.
- Validar paquete: Verifique que el paquete declarado coincida con una lista de permitidos explícita de socios autorizados o paquetes internos.
- Reconstruir: Instancie un nuevo
Intent(Intent.ACTION_VIEW, validatedDataUri). - Aislar: Aplique
intent.addCategory(Intent.CATEGORY_BROWSABLE)para garantizar que solo las actividades navegables puedan resolver la solicitud, establezca indicadores explícitos (FLAG_ACTIVITY_NEW_TASK) y asegúrese de queintent.componenteintent.selectorpermanezcan nulos.
Hacer cumplir listas de permitidos estrictas de esquema, host, ruta y parámetros de consulta
Tanto los esquemas personalizados (myapp://) como las URIs de Intent reconstruidas deben pasar por un validador estricto antes de la ejecución.
Esta implementación de referencia maneja específicamente el esquema myapp:// de la propia aplicación host, vinculando explícitamente la ejecución de intenciones a com.example.app. Para ecosistemas de socios de múltiples aplicaciones, los desarrolladores deben mantener un mapeo estricto de esquema a paquete o priorizar los App Links HTTPS verificados.
El validador verifica:
- Esquema: Coincide con los esquemas personalizados autorizados (
myapp). - Host: Coincide con categorías de enrutamiento predefinidas (
open,product,event). - Prefijo de ruta: Cumple con las estructuras de ruta esperadas (
/detail/,/view/,/main/). - Parámetros de consulta: Aplica claves de consulta permitidas (
target,id,promo,utm_source) y valida que los valores de los parámetros cumplan con los límites de longitud (ej.,caracteres) y conjuntos de caracteres alfanuméricos.
[El usuario toca el enlace dentro del Android WebView]
│
▼
[WebViewClient.shouldOverrideUrlLoading]
│
┌─────────────┴─────────────┐
▼ ▼
[!request.isForMainFrame] [request.isForMainFrame]
│ │
▼ ▼
[Comprobar esquema de submarco] [Inspeccionar esquema de URL]
├─ HTTP(S) -> devolver false │
└─ No web -> devolver true ┌────┴───────────────────────────┐
(Suprimir error) ▼ ▼
[HTTP / HTTPS] [Esquema que no es HTTP(S)]
│ │
▼ ▼
[Comprobar dominio host] [CustomSchemeValidator]
├─ Enlace profundo interno ├─ Válido -> Lanzar Intent fijado
│ -> Enrutador interno └─ Intent -> Reconstruir y respaldo
└─ Web externa
-> Devolver false
Cómo gestionar la visibilidad de paquetes de Android 11 y los respaldos de la tienda de aplicaciones

El cambio de visibilidad de paquetes de Android 11: por qué las consultas previas al vuelo requieren declaración
A partir de Android 11 (nivel de API 30), Google introdujo el filtrado de Visibilidad de paquetes para mejorar la privacidad del usuario. Bajo este marco, la visión de una aplicación sobre otras aplicaciones instaladas está restringida de forma predeterminada.
Es fundamental comprender la distinción entre el lanzamiento de intenciones y la consulta de intenciones:
- Lanzamiento directo de intenciones: Las llamadas estándar a
context.startActivity(intent)no requieren declaraciones de visibilidad de paquetes. Si la aplicación de destino está instalada, el sistema operativo la enruta y la lanza con éxito, independientemente de si es visible para las consultas de paquetes. - Consultas de intención previas al vuelo: Si su arquitectura
WebViewClientrealiza una inspección previa al vuelo, como llamar apackageManager.resolveActivity(intent, 0)opackageManager.queryIntentActivities(intent, 0)para verificar la presencia de la aplicación antes de lanzarla, la consulta devuelvenullo una lista vacía a menos que el paquete o esquema de destino esté declarado enAndroidManifest.xml.
Declaración de elementos <queries> en AndroidManifest.xml para esquemas personalizados y destinos de paquetes
De acuerdo con la Guía para desarrolladores de Android sobre la visibilidad de paquetes en Android 11, las aplicaciones que realizan comprobaciones previas al vuelo antes de la ejecución de la intención deben declarar sus paquetes de destino o firmas de intención en el manifiesto:
<manifest package="com.example.app">
<!-- Declaraciones de consulta previas al vuelo opcionales para el descubrimiento explícito de paquetes -->
<queries>
<package android:name="com.example.partner" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
</manifest>
Si su arquitectura prefiere patrones de lanzamiento estándar, ejecutar startActivity() dentro de un bloque try-catch que capture ActivityNotFoundException proporciona una implementación resistente e independiente de las consultas.
Implementación de cadenas de respaldo: enrutamiento de tienda elegante cuando la aplicación de destino está ausente
Cuando un esquema personalizado interceptado o una URI de Intent no puede lanzarse porque la aplicación de destino no está instalada en el dispositivo del usuario, el WebViewClient debe ejecutar una cadena de respaldo explícita:
- Extraer y vincular la URL de respaldo: Para las URIs
intent://, extraiga el parámetro de cadenaS.browser_fallback_urly valide que apunte a un destino autorizado. Para las URL de Google Play, verifique que el parámetroidcoincida estrictamente con el nombre del paquete de destino (id=targetPackage) para evitar exploits de redirección abierta. - Generación de URI de mercado: Si no hay una URL de respaldo presente, construya una intención de mercado:
market://details?id=com.example.app. - Respaldo de la Web Play Store: Si Google Play está deshabilitado o ausente, capture la
ActivityNotFoundExceptionen la intención de mercado y carguehttps://play.google.com/store/apps/details?id=com.example.app.
Vinculación de parámetros contextuales a través de redirecciones de respaldo con OpoInstall
Cuando los usuarios desinstalados son dirigidos a Google Play, los enlaces de tienda estáticos eliminan los parámetros dinámicos. OpoInstall, una plataforma de atribución móvil y enlaces profundos, cierra esta brecha capturando tokens de referencia y contexto de campaña en el backend de atribución antes de la redirección a la tienda. Verifique el comportamiento de recuperación de parámetros diferidos y las API de inicialización frente a la versión del SDK de Android de OpoInstall implementada en producción.
Tras la instalación posterior y el inicio inicial de la aplicación, el SDK de Android de OpoInstall recupera los parámetros almacenados en caché, lo que permite a la aplicación host restaurar la escena de destino del usuario. Revise la documentación de integración del SDK para obtener instrucciones sobre cómo recibir parámetros diferidos después de la instalación.
Implementación de WebViewClient de producción para una interceptación de esquema segura
Estructuración de la arquitectura de WebViewClient personalizada en Kotlin
Un WebViewClient de producción coordina la verificación de marcos, la creación de listas de permitidos de esquemas, la reconstrucción de intenciones, la extracción de respaldos y el enrutamiento interno. La implementación encapsula las reglas de validación dentro de objetos validadores dedicados (AppLinkRouteValidator y CustomSchemeValidator) para mantener las devoluciones de llamada de navegación limpias y mantenibles.
Implementación de modelos AppLinkRouteValidator y CustomSchemeValidator
Los objetos validadores aplican una seguridad estricta de fallo cerrado:
AppLinkRouteValidatorvalida dominios HTTPS internos, prefijos de ruta permitidos y claves de consulta autorizadas (target,id,promo,utm_source), devolviendo una clase de datosValidatedAppRouteestructurada.CustomSchemeValidatorverifica esquemas personalizados autorizados (myapp), valida hosts permitidos (open,product,event), aplica prefijos de ruta y desinfecta los valores de los parámetros de consulta contra conjuntos de caracteres alfanuméricos y restricciones de longitud (caracteres).
La implementación del código a continuación demuestra cómo configurar declaraciones opcionales de visibilidad de paquetes en AndroidManifest.xml e implementar un CustomWebViewClient seguro y con seguridad de subprocesos en Kotlin con manejo automatizado de respaldos. Los binarios del SDK certificados y los envoltorios de integración se pueden adquirir en el centro de descargas del SDK de OpoInstall.
<!-- AndroidManifest.xml: Declaraciones opcionales de visibilidad de paquetes para consultas de intenciones previas al vuelo (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- Declare paquetes externos y esquemas personalizados solo si utiliza consultas resolveActivity previas al vuelo -->
<queries>
<!-- Autorizar el descubrimiento previo al vuelo para paquetes de socios o de destino específicos -->
<package android:name="com.example.partner" />
<!-- Autorizar la resolución previa al vuelo de esquemas de enlaces profundos personalizados -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<!-- Autorizar la resolución previa al vuelo de dominios de enlaces de aplicaciones HTTPS verificados -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- Componentes de la aplicación -->
</application>
</manifest>
// Android: WebViewClient personalizado con seguridad de límites de marco, validación de lista de permitidos y enrutamiento de respaldo
// Ejemplo de integración de referencia; verificar firmas de métodos contra la arquitectura de Android implementada.
package com.example.app.webview
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
data class ValidatedAppRoute(val path: String, val queryParams: Map<String, String>)
object AppLinkRouteValidator {
private val allowedPrefixes = setOf("/open/", "/product/", "/promo/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): ValidatedAppRoute? {
val path = uri.path ?: ""
if (allowedPrefixes.none { path.startsWith(it) }) {
return null
}
val sanitizedParams = mutableMapOf<String, String>()
for (name in uri.queryParameterNames) {
// Postura de seguridad estricta: rechazar solicitud si existen claves de consulta desconocidas
if (name !in allowedQueryKeys) {
return null
}
val value = uri.getQueryParameter(name) ?: ""
if (value.length <= 64 && value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
sanitizedParams[name] = value
} else {
return null
}
}
return ValidatedAppRoute(path, sanitizedParams)
}
}
object CustomSchemeValidator {
private val allowedSchemes = setOf("myapp")
private val allowedHosts = setOf("open", "product", "event")
private val allowedPathPrefixes = setOf("/detail/", "/view/", "/main/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): Uri? {
val scheme = uri.scheme?.lowercase() ?: return null
val host = uri.host?.lowercase() ?: return null
val path = uri.path ?: ""
if (!allowedSchemes.contains(scheme) || !allowedHosts.contains(host)) {
return null
}
if (allowedPathPrefixes.none { path.startsWith(it) }) {
return null
}
for (name in uri.queryParameterNames) {
if (name !in allowedQueryKeys) return null
val value = uri.getQueryParameter(name) ?: ""
if (value.length > 128 || !value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
return null
}
}
return uri
}
}
class CustomWebViewClient(
private val context: Context,
private val internalRouter: (ValidatedAppRoute) -> Unit
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request ?: return false
// Requisito de seguridad: Restringir el enrutamiento de aplicaciones externas solo a navegaciones de marco principal
if (!request.isForMainFrame) {
val scheme = request.url.scheme?.lowercase()
// Permitir la carga de submarcos HTTP(S) estándar, pero suprimir esquemas que no son web para evitar el secuestro de iframe y errores
return if (scheme == "http" || scheme == "https") {
false
} else {
Log.w("WebViewClient", "Esquema no web suprimido en submarco/iframe: $scheme")
true // Consumir/cancelar sin lanzar una intención externa o mostrar un error de Chromium
}
}
val url = request.url.toString()
return handleUrlNavigation(view, url)
}
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url == null) return false
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
// La devolución de llamada heredada carece de metadatos fiables isForMainFrame; no enrutar esquemas externos
return if (scheme == "http" || scheme == "https") {
false // Permitir la carga de contenido web estándar
} else {
Log.w("WebViewClient", "Esquema externo bloqueado en devolución de llamada heredada: $scheme")
true // Suprimir errores de esquema no controlados sin lanzar intenciones externas
}
}
private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase() ?: return false
// 1. Manejar HTTP / HTTPS y enlaces de aplicaciones internos
if (scheme == "http" || scheme == "https") {
// Comprobar si la URL coincide con el dominio App Link de la propia aplicación host
if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
val validatedRoute = AppLinkRouteValidator.validate(uri)
if (validatedRoute != null) {
Log.i("WebViewClient", "Ruta HTTPS interna validada. Enviando al enrutador interno.")
internalRouter(validatedRoute)
} else {
Log.w("WebViewClient", "Enlace de aplicación interno mal formado o no autorizado rechazado: $uri")
}
return true // Gestionado internamente; cancelar carga de webview
}
return false // Permitir que WebView cargue contenido web estándar de forma nativa
}
// 2. Validar esquemas personalizados con lista de permitidos estricta y fijación de paquetes para evitar secuestros
val validatedUri = CustomSchemeValidator.validate(uri)
if (validatedUri != null) {
val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
`package` = "com.example.app" // Fijar explícitamente el paquete de la aplicación host
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
return launchSafeIntent(pinnedIntent, fallbackAction = {
handleStoreFallback(view, "com.example.app")
})
}
// 3. Gestionar URIs de Intent de Chrome con reconstrucción segura y extracción de respaldo
if (scheme == "intent") {
return try {
val parsed = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
val targetPackage = parsed.`package`
val allowedPackages = setOf("com.example.app", "com.example.partner")
val fallbackUrl = parsed.getStringExtra("browser_fallback_url")
if (targetPackage != null && allowedPackages.contains(targetPackage)) {
val rawDataUri = parsed.data
if (rawDataUri != null) {
val validatedDataUri = CustomSchemeValidator.validate(rawDataUri)
if (validatedDataUri != null) {
// Reconstruir un Intent ACTION_VIEW limpio y dedicado en lugar de ejecutar un objeto analizado sin procesar
val safeIntent = Intent(Intent.ACTION_VIEW, validatedDataUri).apply {
`package` = targetPackage
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
launchSafeIntent(safeIntent, fallbackAction = {
handleIntentFallback(view, fallbackUrl, targetPackage)
})
} else {
Log.w("WebViewClient", "Intención rechazada con URI de datos no válida: $rawDataUri")
true
}
} else {
Log.w("WebViewClient", "Intención rechazada sin carga útil de URI de datos")
true
}
} else {
Log.w("WebViewClient", "Intención rechazada dirigida a un paquete no verificado: $targetPackage")
true // Cancelar intención de paquete no verificado
}
} catch (e: Exception) {
Log.e("WebViewClient", "No se pudo analizar el URI de intent: $url", e)
true // Suprimir errores de esquema no controlados
}
}
// Rechazar esquemas desconocidos para evitar errores net::ERR_UNKNOWN_URL_SCHEME
Log.w("WebViewClient", "Navegación de esquema no verificada bloqueada: $scheme")
return true
}
private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
return try {
context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
Log.w("WebViewClient", "Aplicación de destino no instalada: ${intent.data}. Activando respaldo.")
fallbackAction()
true
} catch (e: SecurityException) {
Log.e("WebViewClient", "Excepción de seguridad al lanzar la intención: ${intent.data}", e)
true
}
}
private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
Log.i("WebViewClient", "Enrutando a la URL de respaldo del navegador autorizada: $fallbackUrl")
view?.loadUrl(fallbackUrl)
} else {
handleStoreFallback(view, targetPackage)
}
}
private fun handleStoreFallback(view: WebView?, packageName: String) {
val marketUri = Uri.parse("market://details?id=$packageName")
val marketIntent = Intent(Intent.ACTION_VIEW, marketUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(marketIntent)
} catch (e: ActivityNotFoundException) {
val webStoreUrl = "https://play.google.com/store/apps/details?id=$packageName"
Log.i("WebViewClient", "Aplicación Play Store ausente. Cargando URL de la tienda web: $webStoreUrl")
view?.loadUrl(webStoreUrl)
}
}
private fun isAuthorizedFallbackUrl(url: String, targetPackage: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
val host = uri.host?.lowercase()
if (scheme != "https" || host == null) return false
return when (host) {
"play.google.com" -> {
// Hacer cumplir que la URL de respaldo de Google Play apunte específicamente al paquete de destino
uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
}
"app.example.com", "www.example.com" -> {
// Validar ruta de respaldo de dominio propio
AppLinkRouteValidator.validate(uri) != null
}
else -> false
}
}
}
Ejecución con seguridad de subprocesos: envío de rutas internas y gestión de estados de ciclo de vida
Cuando AppLinkRouteValidator confirma una ruta interna válida, las transiciones de la interfaz de usuario deben ejecutarse en el subproceso principal de la interfaz de usuario de Android. Si internalRouter activa fragmentos, transiciones de actividad o navegación de Compose, envuelva la llamada de envío dentro de runOnUiThread o una corrutina de ciclo de vida de subproceso principal para evitar fallos de subprocesos.
Matriz de soluciones de diagnóstico y errores de redirección de Android WebView
Guía completa de solución de problemas de enlaces profundos de Android WebView
La matriz a continuación detalla las firmas de error comunes encontradas al manejar esquemas personalizados en WebViews de Android, junto con las causas raíz principales y las remediaciones de ingeniería:
| Firma de error / Síntoma | Causa raíz principal | Niveles de API afectados | Punto de control de diagnóstico | Remediación recomendada |
|---|---|---|---|---|
net::ERR_UNKNOWN_URL_SCHEME |
Esquema que no es web no gestionado en el cargador de WebView | Todos los niveles de API | shouldOverrideUrlLoading falta o devuelve false |
Anular delegado, interceptar esquema, devolver true |
ActivityNotFoundException |
Aplicación de destino ausente en el dispositivo | Todos los niveles de API | context.startActivity() no controlado |
Capturar excepción y ejecutar S.browser_fallback_url o ruta de Play Store |
| Secuestro de intención de Iframe | Submarco activando intención externa | API 24+ | request.isForMainFrame no verificado |
Proteger con if (!request.isForMainFrame) y suprimir esquemas que no son web |
| La consulta previa al vuelo devuelve nulo | Filtro de visibilidad de paquetes de Android 11+ | API 30+ (Android 11+) | Llamar a resolveActivity sin declaración de manifiesto |
Declarar <queries> en manifiesto o usar startActivity try-catch directamente |
| Bucle de recarga de enlace de aplicación interno | Enlace de aplicación host redirigido a WebView | Todos los niveles de API | Dominio de host no gestionado en delegado | Interceptar dominio propio, validar ruta, enviar a enrutador nativo |
Preguntas frecuentes (FAQ)
¿Por qué mi Android WebView se bloquea al abrir esquemas de URL personalizados?
¿Cómo gestiono de forma segura las URL intent:// en un WebView sin vulnerabilidades de seguridad?
¿Cómo afecta la visibilidad de paquetes de Android 11 a la interceptación de enlaces profundos en WebViews?
Resumen y marco de decisión
Gestionar enlaces profundos y esquemas personalizados dentro de los WebViews de Android requiere cerrar la brecha entre los sandboxes de renderizado web y el enrutamiento de intenciones del sistema operativo nativo. Depender del comportamiento predeterminado de WebView da como resultado errores de protocolo no controlados, una mala experiencia de usuario y posibles vulnerabilidades de seguridad.
Al implementar un WebViewClient robusto que verifique los límites del marco, valide esquemas personalizados contra listas de permitidos explícitas, reconstruya intenciones ACTION_VIEW seguras y gestione respaldos de tienda elegantes, los equipos de ingeniería reducen los errores de manejo de esquemas de WebView y admiten una navegación más resistente. La integración de un enrutamiento confiable del lado del cliente con marcos integrales de atribución móvil reduce la fricción de enrutamiento a través de los flujos de atribución y respalda viajes de usuario más consistentes.
Para explorar arquitecturas nativas de enlaces profundos de Android y enrutamiento de parámetros, consulte la documentación de integración del SDK, descargue las bibliotecas de cliente desde el centro de descargas del SDK de OpoInstall, explore la referencia de implementación de atribución móvil o registre su aplicación en la consola de desarrollador de OpoInstall.
Materiales relacionados
- Conceptos: Navegación de Android WebView, interceptación de esquema de URL personalizado, reconstrucción de URI de Intent, visibilidad de paquetes
- Tecnologías: Android WebViewClient, motor web Chromium, Android Debug Bridge, SDK de Android de OpoInstall
- Estándares: Identificador uniforme de recursos IETF RFC 3986, especificación de Intent de Android, Guía de pruebas de seguridad de aplicaciones móviles de OWASP (MASTG)
- APIs:
shouldOverrideUrlLoadingde Android,WebResourceRequest.isForMainFrame,Intent.parseUri,PackageManager.queryIntentActivities - Documentación y referencias oficiales:
- Referencia de desarrolladores de Android sobre WebViewClient shouldOverrideUrlLoading
- Guía para desarrolladores de Android sobre WebResourceRequest
- Guía para desarrolladores de Android sobre la visibilidad de paquetes en Android 11
- Guía de Chrome para desarrolladores sobre URIs de Intent de Android
- Guía de pruebas de seguridad de aplicaciones móviles de OWASP sobre enlaces profundos inseguros
Share this article


