Cómo interceptar esquemas de URL personalizados en redirecciones de Android WebView

opoinstall
2026-09-21
5 min read

¿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

Los esquemas personalizados deben abandonar la canalización de carga del WebView antes de que falle Chromium.

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:

  1. 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.
  2. 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).
  3. Evaluación de canalización: Si shouldOverrideUrlLoading() devuelve false o 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.
  4. 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 interfaz WebResourceRequest ofrece 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

Las comprobaciones del marco principal evitan que la navegación de iframe inicie rutas de aplicaciones externas.

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.isForMainFrame garantiza 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 devolver false para continuar con la renderización en marco, mientras que los esquemas que no son HTTP(S) deben devolver true para suprimir la ejecución sin lanzar errores.
  • Distinción de gestos del usuario: Aunque isForMainFrame confirma 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úe request.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 donde hasGesture() 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. El WebView continú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ándar http:// y https:// que deben mostrarse dentro del navegador integrado.
  • Devolver true: Informa al marco que la aplicación host ha interceptado y gestionado la URL. El WebView cancela inmediatamente su proceso de carga interno. Este valor de retorno es obligatorio al lanzar un Intent externo, 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.

Los URIs de intent analizados deben validarse y reconstruirse antes del envío de Android.

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:

  1. Extraer datos: Analice el URI y extraiga el paquete de destino verificado y el URI de datos.
  2. Validar paquete: Verifique que el paquete declarado coincida con una lista de permitidos explícita de socios autorizados o paquetes internos.
  3. Reconstruir: Instancie un nuevo Intent(Intent.ACTION_VIEW, validatedDataUri).
  4. 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 que intent.component e intent.selector permanezcan 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., ≤128\le 128 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

La visibilidad del paquete afecta a las consultas previas al vuelo, mientras que los fallos de lanzamiento utilizan respaldos validados.

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 WebViewClient realiza una inspección previa al vuelo, como llamar a packageManager.resolveActivity(intent, 0) o packageManager.queryIntentActivities(intent, 0) para verificar la presencia de la aplicación antes de lanzarla, la consulta devuelve null o una lista vacía a menos que el paquete o esquema de destino esté declarado en AndroidManifest.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:

  1. Extraer y vincular la URL de respaldo: Para las URIs intent://, extraiga el parámetro de cadena S.browser_fallback_url y valide que apunte a un destino autorizado. Para las URL de Google Play, verifique que el parámetro id coincida estrictamente con el nombre del paquete de destino (id=targetPackage) para evitar exploits de redirección abierta.
  2. 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.
  3. Respaldo de la Web Play Store: Si Google Play está deshabilitado o ausente, capture la ActivityNotFoundException en la intención de mercado y cargue https://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:

  • AppLinkRouteValidator valida dominios HTTPS internos, prefijos de ruta permitidos y claves de consulta autorizadas (target, id, promo, utm_source), devolviendo una clase de datos ValidatedAppRoute estructurada.
  • CustomSchemeValidator verifica 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 (≤128\le 128 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?
Un esquema personalizado produce `net::ERR_UNKNOWN_URL_SCHEME` cuando la URL permanece en la canalización de carga del WebView en lugar de ser interceptada por la aplicación host. Con un `WebViewClient` personalizado, devuelva `true` después de gestionar o suprimir de forma segura un esquema que no sea web; devuelva `false` solo cuando el WebView deba continuar con la navegación web normal. Si la aplicación de destino está ausente, llamar a `startActivity()` sin un bloque `try-catch` arroja una `ActivityNotFoundException` no controlada.
¿Cómo gestiono de forma segura las URL intent:// en un WebView sin vulnerabilidades de seguridad?
Para gestionar las URIs `intent://` de forma segura, analice la URI utilizando `Intent.parseUri(url, Intent.URI_INTENT_SCHEME)`, valide que el paquete de destino y la URI de datos coincidan con listas de permitidos estrictas, y construya un nuevo `Intent(Intent.ACTION_VIEW, safeDataUri)` limpio. Añada explícitamente `Intent.CATEGORY_BROWSABLE` para garantizar que solo las actividades navegables gestionen la intención, elimine componentes peligrosos e implemente una gestión de URL de respaldo validada vinculada al paquete de destino.
¿Cómo afecta la visibilidad de paquetes de Android 11 a la interceptación de enlaces profundos en WebViews?
La visibilidad de paquetes de Android 11 no impide que `startActivity()` inicie actividades externas coincidentes. Sin embargo, si su arquitectura `WebViewClient` realiza comprobaciones previas al vuelo utilizando `resolveActivity()` o `queryIntentActivities()` antes de intentar un lanzamiento, debe declarar el nombre del paquete de destino o el esquema de intención personalizado dentro del elemento `<queries>` en su archivo `AndroidManifest.xml`.

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

Share this article