Como interceptar esquemas de URL personalizados em redirecionamentos de WebView no Android

opoinstall
2026-09-21
5 min read

Como lidar com deep linking e esquemas personalizados em uma WebView Android? O tratamento de deep links em uma WebView Android requer estender o WebViewClient, sobrescrever o shouldOverrideUrlLoading para verificar o isForMainFrame, validar esquemas personalizados contra uma lista de permissões (allowlist) e iniciar com segurança intents externas autorizadas para evitar que esquemas não web permaneçam no pipeline de carregamento da WebView.

A interceptação de deep links em WebView Android é a prática programática de sobrescrever callbacks de navegação dentro de um WebViewClient personalizado para capturar protocolos não HTTP, como esquemas de URI personalizados e Chrome Intent URIs. Ao avaliar metadados de solicitação e validar payloads de destino, o aplicativo hospedeiro executa lançamentos de Intent nativos enquanto evita falhas por protocolos não tratados.

Termo Definição Entidade Relacionada Papel da Intenção de Busca
WebView Um componente de visualização baseado em Chromium embutido que renderiza conteúdo web dentro de aplicativos Android. Android SDK Informativo / Comercial
Esquema de URL Personalizado Um protocolo de URI proprietário que permite que links web direcionem para aplicativos nativos. Roteamento de Deep Link Técnico / Informativo
Web para App O processo de transição de usuários de páginas web para interfaces de aplicativos nativos. Mobile Deep Linking Informativo

Esquemas personalizados devem sair do pipeline de carregamento da WebView antes que o Chromium falhe.

Por que as WebViews Android lançam erros net::ERR_UNKNOWN_URL_SCHEME

Limitações do protocolo Chromium: Por que as WebViews reconhecem nativamente apenas esquemas web padrão

O componente WebView do Android é alimentado por um motor de renderização Chromium subjacente, configurado principalmente para analisar e exibir protocolos de recursos web padrão: http://, https://, file://, data://, about: e javascript:.

Por padrão, quando nenhum WebViewClient personalizado é atribuído, o framework Android delega URLs não tratados ao Activity Manager do sistema. No entanto, uma vez que um aplicativo fornece um WebViewClient personalizado, esquemas não HTTP(S) (como myapp://product/detail/1024, alipay:// ou whatsapp://) são roteados diretamente para os callbacks de navegação do cliente. Se o aplicativo hospedeiro permitir que um esquema personalizado permaneça dentro do pipeline de carregamento da WebView em vez de interceptá-lo, o motor Chromium tenta resolver o esquema personalizado como um recurso web comum. Como protocolos não padrão não podem ser resolvidos via DNS ou camadas de transporte de servidor web, o motor apresenta uma falha de navegação de baixo nível.

A anatomia da falha net::ERR_UNKNOWN_URL_SCHEME: Do toque do usuário à falha de carregamento de recurso web

A transição de uma interação do usuário para uma página de erro ocorre em uma sequência rápida e determinística:

  1. Gatilho do Usuário: Um usuário toca em um botão de call-to-action ou hiperlink dentro de uma landing page H5 embutida.
  2. Solicitação de Navegação: O motor web constrói uma solicitação de navegação contendo a string de URI de destino (por exemplo, myapp://promo/summer).
  3. Avaliação de Pipeline: Se shouldOverrideUrlLoading() retornar false ou falhar ao interceptar a URI não HTTP(S), o motor Chromium trata a URL como um carregamento de recurso web padrão.
  4. Despacho de Erro: Ao não encontrar um manipulador interno para o protocolo, o motor dispara onReceivedError() e substitui o conteúdo da WebView pelo modelo de erro padrão do Chromium: net::ERR_UNKNOWN_URL_SCHEME.

Esta página de erro interrompe a jornada do usuário, impede o rastreamento de conversão e degrada a estabilidade percebida do aplicativo hospedeiro.

O risco de falhas ActivityNotFoundException não tratadas na ausência do aplicativo de destino

Quando desenvolvedores tentam pela primeira vez resolver net::ERR_UNKNOWN_URL_SCHEME, um erro comum é analisar a string de URI e chamar context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) sem o tratamento de exceção adequado.

Se o usuário não tiver o aplicativo de destino instalado em seu dispositivo, o sistema operacional Android não consegue encontrar uma Activity elegível para resolver a Intent. Na execução padrão do Android, chamar startActivity() sem um manipulador correspondente lança uma android.content.ActivityNotFoundException não tratada. Se esta exceção não for capturada dentro de um bloco try-catch estruturado, todo o processo do aplicativo hospedeiro é encerrado abruptamente, resultando em um crash fatal.

Como o shouldOverrideUrlLoading intercepta protocolos de deep link

Evolução da API: Comparando sobrecargas do WebResourceRequest na API 24+ com callbacks de String legados

O WebViewClient do Android fornece o callback shouldOverrideUrlLoading para permitir que aplicativos hospedeiros inspecionem e interceptem solicitações de navegação antes que o motor web as execute. A assinatura e as capacidades deste método evoluíram ao longo dos níveis de API do Android:

  • API 24+ (Android 7.0 e superior): Sobrescreve shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean. A interface WebResourceRequest fornece metadados contextuais ricos, incluindo a URL de destino (request.url), o método de solicitação HTTP (request.method), cabeçalhos de solicitação e sinalizadores críticos de hierarquia de frame (request.isForMainFrame, request.hasGesture()).
  • Sobrecarga Legada (Obsoleta na API 24): Sobrescreve shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean. Este callback legado recebe apenas a string da URL bruta sem metadados sobre o frame solicitante ou o estado de interação do usuário.

De acordo com a Referência de Desenvolvedores Android sobre WebViewClient shouldOverrideUrlLoading, aplicativos modernos devem implementar a sobrecarga WebResourceRequest. Ao suportar versões legadas do Android, o callback de string obsoleto deve ser configurado de forma conservadora para evitar bypasses de segurança.

A criticidade de segurança de request.isForMainFrame: Bloqueando hijacking de subframes e iframes

Verificações de frame principal impedem que a navegação em iframe inicie rotas de aplicativos externos.

De acordo com o Guia de Desenvolvedores Android sobre WebResourceRequest, o callback shouldOverrideUrlLoading pode ser disparado para subframes, iframes e redirecionamentos de recursos em segundo plano, bem como para navegações de documentos de nível superior.

Impor request.isForMainFrame é essencial para a segurança do aplicativo:

  • Isolamento de Fronteira de Frame: Verificar request.isForMainFrame garante que subframes e iframes não possam disparar lançamentos de aplicativos externos ou intents de esquemas personalizados. Se um subframe tentar navegação, carregamentos HTTP(S) padrão devem retornar false para continuar a renderização in-frame, enquanto esquemas não HTTP(S) devem retornar true para suprimir a execução sem lançar erros.
  • Distinção de Gesto do Usuário: Embora isForMainFrame confirme que a solicitação se aplica ao documento primário, não prova a ativação do usuário. Se sua arquitetura exigir verificação de gesto do usuário, avalie request.hasGesture() ou aplique modais de confirmação em nível de aplicativo, observando que a documentação do Android destaca certos fluxos válidos do usuário onde hasGesture() pode retornar false.

Diferenciando navegação web normal de handoffs de deep link: Quando retornar false vs true

O valor de retorno booleano de shouldOverrideUrlLoading controla o ciclo de vida de renderização da WebView:

  • Retornar false: Informa ao framework Android que o aplicativo hospedeiro não tratou a URL. A WebView continua a navegação web padrão, carregando a URL dentro de seu contêiner Chromium interno. Este valor de retorno é obrigatório para páginas web http:// e https:// padrão que devem ser exibidas dentro do navegador embutido.
  • Retornar true: Informa ao framework que o aplicativo hospedeiro interceptou e tratou a URL. A WebView cancela imediatamente seu processo de carregamento interno. Este valor de retorno é obrigatório ao lançar uma Intent externa, rotear para uma atividade nativa ou suprimir um esquema não autorizado.

Tratando App Links verificados do aplicativo hospedeiro dentro do contêiner embutido

Quando um link dentro da WebView aponta para o próprio domínio HTTPS verificado do aplicativo hospedeiro (como https://app.example.com/product/1024), executar a navegação padrão faz com que a WebView carregue a versão web da página do produto.

Para oferecer uma experiência nativa, o WebViewClient deve inspecionar URLs HTTPS recebidas. Se o host corresponder ao domínio associado do aplicativo, o cliente intercepta a solicitação, valida o caminho e os parâmetros de consulta contra um validador de rota interno (AppLinkRouteValidator) e despacha os dados diretamente para o roteador nativo (internalRouter(validatedRoute)), retornando true para cancelar a renderização web redundante.

Estruturando manipuladores de Intent seguros para esquemas personalizados e URIs de Intent

Dissecando a sintaxe de Intent URI do Chrome: Esquemas, Pacotes e S.browser_fallback_url

No Android, páginas web frequentemente utilizam Chrome Intent URIs para estruturar deep links nativos com mecanismos de fallback de navegador embutidos. Uma Intent URI usa o esquema intent:// e codifica parâmetros de roteamento, destinos de pacote e URLs de fallback em uma string 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

Conforme documentado no Guia de Desenvolvedores do Chrome sobre Intent URIs no Android, a URI é dividida em componentes distintos:

  • Host e Caminho (product/detail/1024): Define o destino interno do aplicativo.
  • scheme=myapp: Especifica o protocolo personalizado subjacente.
  • package=com.example.app: Restringe a resolução de intent exclusivamente ao pacote Android especificado.
  • S.browser_fallback_url=...: Um extra opcional de String definindo uma URL web ou de loja explícita para carregar caso o aplicativo de destino não esteja instalado.

O perigo de executar Intents analisadas brutas: Mitigando vulnerabilidades de Injeção de Intent

Um antipadrão comum de implementação é analisar uma URI recebida com Intent.parseUri(url, Intent.URI_INTENT_SCHEME) e passar a Intent resultante diretamente para context.startActivity(intent).

De acordo com a Orientação do Guia de Testes de Segurança de Aplicativos Móveis da OWASP sobre Deep Links Inseguros, executar intents analisadas brutas expõe o aplicativo a ataques de Injeção de Intent. Um atacante que controle a página web poderia criar uma Intent URI contendo destinos de componentes explícitos maliciosos (component=com.example.app/.InternalAdminActivity), ações sensíveis ou extras de intent privilegiados (S.is_admin=true). A WebView desserializaria esses parâmetros e iniciaria atividades não exportadas ou internas, contornando barreiras de autenticação.

URIs de intent analisadas devem ser validadas e reconstruídas antes do despacho no Android.

Reconstrução segura de Intent: Removendo componentes e restringindo ações a ACTION_VIEW

Para eliminar vulnerabilidades de injeção de Intent, aplicativos hospedeiros nunca devem lançar uma Intent desserializada bruta diretamente. Em vez disso, o WebViewClient deve analisar a URI, extrair apenas o payload de dados verificado e reconstruir uma Intent dedicada e limpa:

  1. Extrair Dados: Analise a URI e extraia o pacote de destino verificado e a URI de dados.
  2. Validar Pacote: Verifique se o pacote declarado corresponde a uma lista de permissões explícita de parceiros autorizados ou pacotes internos.
  3. Reconstruir: Instancie uma nova Intent(Intent.ACTION_VIEW, validatedDataUri).
  4. Isolar: Aplique intent.addCategory(Intent.CATEGORY_BROWSABLE) para garantir que apenas atividades navegáveis possam resolver a solicitação, defina sinalizadores explícitos (FLAG_ACTIVITY_NEW_TASK) e garanta que intent.component e intent.selector permaneçam nulos.

Impondo listas de permissões estritas para Esquema, Host, Caminho e Parâmetros de Consulta

Tanto esquemas personalizados (myapp://) quanto Intent URIs reconstruídas devem passar por um validador estrito antes da execução.

Esta implementação de referência trata especificamente o esquema myapp:// do próprio aplicativo hospedeiro, fixando explicitamente a execução da intent para com.example.app. Para ecossistemas de parceiros multi-app, os desenvolvedores devem manter um mapeamento rigoroso de esquema para pacote ou priorizar App Links HTTPS verificados.

O validador verifica:

  • Esquema: Corresponde a esquemas personalizados autorizados (myapp).
  • Host: Corresponde a categorias de roteamento predefinidas (open, product, event).
  • Prefixo de Caminho: Está em conformidade com as estruturas de rota esperadas (/detail/, /view/, /main/).
  • Parâmetros de Consulta: Impõe chaves de consulta permitidas (target, id, promo, utm_source) e valida se os valores dos parâmetros cumprem limites de comprimento (por exemplo, ≤128\le 128 caracteres) e conjuntos de caracteres alfanuméricos.
[Usuário toca no link dentro da WebView Android]
                   │
                   ▼
[WebViewClient.shouldOverrideUrlLoading]
                   │
     ┌─────────────┴─────────────┐
     ▼                           ▼
[!request.isForMainFrame]   [request.isForMainFrame]
     │                           │
     ▼                           ▼
[Verificar esquema de Subframe]     [Inspecionar esquema de URL]
├─ HTTP(S) -> retornar false       │
└─ Não-Web -> retornar true   ┌────┴───────────────────────────┐
   (Suprimir Erro)         ▼                                ▼
                     [HTTP / HTTPS]                 [Esquema não-HTTP(S)]
                            │                                │
                            ▼                                ▼
                   [Verificar Domínio Host]              [CustomSchemeValidator]
                   ├─ App Link Interno             ├─ Válido -> Lançar Intent Pinned
                   │  -> Roteador Interno            └─ Intent -> Reconstruir & Fallback
                   └─ Web Externo
                      -> Retornar false

Como tratar a Visibilidade de Pacotes do Android 11 e Fallbacks de Loja de Aplicativos

A visibilidade do pacote afeta consultas de pré-vôo, enquanto falhas de lançamento usam fallbacks validados.

A mudança na Visibilidade de Pacotes do Android 11: Por que consultas de pré-vôo exigem declaração

A partir do Android 11 (nível de API 30), o Google introduziu a filtragem de Visibilidade de Pacote para aumentar a privacidade do usuário. Sob este framework, a visão de um aplicativo sobre outros aplicativos instalados é restrita por padrão.

É fundamental entender a distinção entre lançar uma intent e consultar uma intent:

  • Lançamento Direto de Intent: Chamadas padrão para context.startActivity(intent) não exigem declarações de visibilidade de pacote. Se o aplicativo de destino estiver instalado, o sistema operacional roteia e o inicia com sucesso, independentemente de estar visível para consultas de pacotes.
  • Consultas de Intent de Pré-vôo: Se a sua arquitetura de WebViewClient realiza inspeção de pré-vôo—como chamar packageManager.resolveActivity(intent, 0) ou packageManager.queryIntentActivities(intent, 0) para verificar a presença do aplicativo antes de lançar—a consulta retorna null ou uma lista vazia, a menos que o pacote ou esquema de destino seja declarado no AndroidManifest.xml.

Declarando elementos <queries> no AndroidManifest.xml para esquemas personalizados e destinos de pacote

De acordo com o Guia de Desenvolvedores Android sobre Visibilidade de Pacotes no Android 11, aplicativos que realizam verificações de pré-vôo antes da execução da intent devem declarar seus pacotes de destino ou assinaturas de intent no manifesto:

<manifest package="com.example.app">
    <!-- Declarações de consulta de pré-vôo opcionais para descoberta explícita de pacotes -->
    <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>

Se a sua arquitetura prefere padrões de lançamento padrão, executar startActivity() dentro de um bloco try-catch que captura ActivityNotFoundException fornece uma implementação resiliente e independente de consulta.

Implementando cadeias de fallback: Roteamento gracioso para lojas quando o aplicativo de destino está ausente

Quando um esquema personalizado ou Intent URI interceptado não pode ser iniciado porque o aplicativo de destino não está instalado no dispositivo do usuário, o WebViewClient deve executar uma cadeia de fallback explícita:

  1. Extrair e Vincular URL de Fallback: Para URIs intent://, extraia o parâmetro de string S.browser_fallback_url e valide se ele aponta para um destino autorizado. Para URLs do Google Play, verifique se o parâmetro id corresponde estritamente ao nome do pacote de destino (id=targetPackage) para evitar exploits de redirecionamento aberto.
  2. Geração de URI de Mercado: Se nenhuma URL de fallback estiver presente, construa uma intent de mercado: market://details?id=com.example.app.
  3. Fallback para a Google Play Web: Se o Google Play estiver desativado ou ausente, capture a ActivityNotFoundException na intent de mercado e carregue https://play.google.com/store/apps/details?id=com.example.app.

Vinculando parâmetros contextuais entre redirecionamentos de fallback com OpoInstall

Quando usuários não instalados são roteados para o Google Play, links estáticos de loja descartam parâmetros dinâmicos. O OpoInstall, uma plataforma de atribuição móvel e deep linking, preenche essa lacuna capturando tokens de referência e contexto de campanha no backend de atribuição antes do redirecionamento para a loja. Verifique o comportamento de recuperação de parâmetros diferidos e as APIs de inicialização em relação à versão do SDK Android do OpoInstall implantada em produção.

Após a instalação subsequente e o lançamento inicial do aplicativo, o SDK Android do OpoInstall recupera os parâmetros em cache, permitindo que o aplicativo hospedeiro restaure a cena de destino do usuário. Revise a documentação de integração do SDK para obter instruções sobre como receber parâmetros diferidos pós-instalação.

Implementação de WebViewClient em produção para interceptação segura de esquemas

Estruturando a arquitetura do WebViewClient personalizado em Kotlin

Um WebViewClient de produção coordena a verificação de frames, criação de listas de permissões, reconstrução de intent, extração de fallback e roteamento interno. A implementação encapsula regras de validação dentro de objetos validadores dedicados (AppLinkRouteValidator e CustomSchemeValidator) para manter os callbacks de navegação limpos e sustentáveis.

Implementando modelos AppLinkRouteValidator e CustomSchemeValidator

Os objetos validadores impõem uma segurança rigorosa de fail-closed:

  • AppLinkRouteValidator valida domínios HTTPS internos, prefixos de caminho permitidos e chaves de consulta autorizadas (target, id, promo, utm_source), retornando uma data class ValidatedAppRoute estruturada.
  • CustomSchemeValidator verifica esquemas personalizados autorizados (myapp), valida hosts permitidos (open, product, event), impõe prefixos de caminho e sanitiza valores de parâmetros de consulta contra conjuntos de caracteres alfanuméricos e limites de comprimento (≤128\le 128 caracteres).

A implementação de código abaixo demonstra como configurar declarações opcionais de visibilidade de pacote no AndroidManifest.xml e implementar um CustomWebViewClient seguro e thread-safe em Kotlin com tratamento automático de fallback. Binários de SDK certificados e wrappers de integração podem ser adquiridos no centro de downloads do SDK do OpoInstall.

<!-- AndroidManifest.xml: Declarações de Visibilidade de Pacote Opcionais para Consultas de Intent de Pré-vôo (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <!-- Declare pacotes externos e esquemas personalizados apenas se estiver usando consultas resolveActivity de pré-vôo -->
    <queries>
        <!-- Autorizar descoberta de pré-vôo para pacotes de parceiros ou destino específicos -->
        <package android:name="com.example.partner" />
        
        <!-- Autorizar resolução de pré-vôo de esquemas de deep linking personalizados -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="myapp" />
        </intent>
        
        <!-- Autorizar resolução de pré-vôo de domínios HTTPS de App Link 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 do aplicativo -->
    </application>
</manifest>
// Android: Custom WebViewClient com Segurança de Limite de Frame, Validação de Listas de Permissão e Roteamento de Fallback
// Exemplo de integração de referência; verifique as assinaturas de método em relação à arquitetura Android implantada.
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 segurança estrita: rejeitar solicitação se existirem chaves de consulta desconhecidas
            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 segurança: Restringir roteamento de app externo apenas para navegações de frame principal
        if (!request.isForMainFrame) {
            val scheme = request.url.scheme?.lowercase()
            // Permitir carregamento padrão HTTP(S) em subframes, mas suprimir esquemas não-web para evitar hijacking de iframe & erros
            return if (scheme == "http" || scheme == "https") {
                false
            } else {
                Log.w("WebViewClient", "Esquema não-web suprimido em subframe/iframe: $scheme")
                true // Consumir/cancelar sem lançar intent externa ou mostrar erro do Chromium
            }
        }

        val url = request.url.toString()
        return handleUrlNavigation(view, url)
    }

    @Deprecated("Obsoleto em Java")
    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
        if (url == null) return false

        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase()

        // Callback legado carece de metadados isForMainFrame confiáveis; não rotear esquemas externos
        return if (scheme == "http" || scheme == "https") {
            false // Permitir carregamento de conteúdo web padrão
        } else {
            Log.w("WebViewClient", "Esquema externo bloqueado no callback legado: $scheme")
            true // Suprimir erros de esquema não tratado sem lançar intents externas
        }
    }

    private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase() ?: return false

        // 1. Tratar HTTP / HTTPS e App Links Internos
        if (scheme == "http" || scheme == "https") {
            // Verificar se a URL corresponde ao domínio de App Link do próprio aplicativo hospedeiro
            if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
                val validatedRoute = AppLinkRouteValidator.validate(uri)
                if (validatedRoute != null) {
                    Log.i("WebViewClient", "Rota HTTPS interna validada. Despachando para o roteador interno.")
                    internalRouter(validatedRoute)
                } else {
                    Log.w("WebViewClient", "App Link interno malformado ou não autorizado rejeitado: $uri")
                }
                return true // Tratado internamente; cancelar carregamento da webview
            }
            return false // Permitir que a WebView carregue conteúdo web padrão nativamente
        }

        // 2. Validar esquemas personalizados com lista de permissões estrita e fixação de pacote para evitar hijacking
        val validatedUri = CustomSchemeValidator.validate(uri)
        if (validatedUri != null) {
            val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
                `package` = "com.example.app" // Fixar explicitamente o pacote do aplicativo hospedeiro
                addCategory(Intent.CATEGORY_BROWSABLE)
                flags = Intent.FLAG_ACTIVITY_NEW_TASK
            }
            return launchSafeIntent(pinnedIntent, fallbackAction = {
                handleStoreFallback(view, "com.example.app")
            })
        }

        // 3. Tratar Chrome Intent URIs com reconstrução segura e extração de fallback
        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 uma Intent ACTION_VIEW limpa e dedicada, em vez de executar o objeto analisado bruto
                            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", "Intent rejeitada com URI de dados inválida: $rawDataUri")
                            true
                        }
                    } else {
                        Log.w("WebViewClient", "Intent rejeitada por falta de payload de URI de dados")
                        true
                    }
                } else {
                    Log.w("WebViewClient", "Intent rejeitada por direcionar para um pacote não verificado: $targetPackage")
                    true // Cancelar intent de pacote não verificado
                }
            } catch (e: Exception) {
                Log.e("WebViewClient", "Falha ao analisar URI de intent: $url", e)
                true // Suprimir erros de esquema não tratado
            }
        }

        // Rejeitar esquemas desconhecidos para evitar erros net::ERR_UNKNOWN_URL_SCHEME
        Log.w("WebViewClient", "Navegação de esquema não verificado bloqueada: $scheme")
        return true
    }

    private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
        return try {
            context.startActivity(intent)
            true
        } catch (e: ActivityNotFoundException) {
            Log.w("WebViewClient", "Aplicativo de destino não instalado: ${intent.data}. Disparando fallback.")
            fallbackAction()
            true
        } catch (e: SecurityException) {
            Log.e("WebViewClient", "Exceção de segurança ao lançar intent: ${intent.data}", e)
            true
        }
    }

    private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
        if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
            Log.i("WebViewClient", "Roteando para URL de fallback de 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", "Aplicativo Play Store ausente. Carregando URL da loja 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" -> {
                // Impor que a URL de fallback do Google Play aponte especificamente para o pacote de destino
                uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
            }
            "app.example.com", "www.example.com" -> {
                // Validar rota de fallback de domínio próprio
                AppLinkRouteValidator.validate(uri) != null
            }
            else -> false
        }
    }
}

Execução Thread-Safe: Despachando rotas internas e gerenciando estados de ciclo de vida

Quando AppLinkRouteValidator confirma uma rota interna válida, as transições de UI devem ser executadas na thread principal de UI do Android. Se internalRouter disparar fragmentos, transições de atividade ou navegação Compose, envolva a chamada de despacho dentro de runOnUiThread ou uma corrotina de ciclo de vida de thread principal para evitar travamentos de thread.

Erros de Redirecionamento de WebView Android e Matriz de Soluções de Diagnóstico

Guia Completo de Solução de Problemas de Deep Linking em WebView Android

A matriz abaixo detalha assinaturas de erro comuns encontradas ao manipular esquemas personalizados em WebViews Android, juntamente com as causas raiz primárias e remediações de engenharia:

Assinatura de Erro / Sintoma Causa Raiz Primária Níveis de API Afetados Ponto de Checagem de Diagnóstico Remediação Recomendada
net::ERR_UNKNOWN_URL_SCHEME Esquema não-web não tratado no carregador da WebView Todos os níveis de API shouldOverrideUrlLoading ausente ou retorna false Sobrescrever delegado, interceptar esquema, retornar true
ActivityNotFoundException App de destino ausente no dispositivo Todos os níveis de API context.startActivity() não tratado Capturar exceção e executar S.browser_fallback_url ou rota da Play Store
Hijacking de Intent em Iframe Subframe disparando intent externa API 24+ request.isForMainFrame não verificado Proteger com if (!request.isForMainFrame) e suprimir esquemas não-web
Consulta de Pré-vôo retorna Null Filtro de visibilidade de pacote do Android 11+ API 30+ (Android 11+) Chamada resolveActivity sem declaração no manifesto Declarar <queries> no manifesto ou usar startActivity com try-catch diretamente
Loop de Recarregamento de App Link Interno App Link interno redirecionado para a WebView Todos os níveis de API Domínio próprio não tratado no delegado Interceptar domínio próprio, validar rota, despachar para roteador nativo

Perguntas Frequentes (FAQ)

Por que minha WebView Android trava ao abrir esquemas de URL personalizados?
Um esquema personalizado produz `net::ERR_UNKNOWN_URL_SCHEME` quando a URL permanece no pipeline de carregamento da WebView em vez de ser interceptada pelo aplicativo hospedeiro. Com um `WebViewClient` personalizado, retorne `true` após tratar ou suprimir com segurança um esquema não web; retorne `false` apenas quando a WebView deve continuar a navegação web normal. Se o aplicativo de destino estiver ausente, chamar `startActivity()` sem um bloco `try-catch` lança uma `ActivityNotFoundException` não tratada.
Como trato URLs intent:// em uma WebView de forma segura, sem vulnerabilidades?
Para tratar URIs `intent://` com segurança, analise a URI usando `Intent.parseUri(url, Intent.URI_INTENT_SCHEME)`, valide se o pacote de destino e a URI de dados correspondem a listas de permissão rigorosas e construa uma nova `Intent(Intent.ACTION_VIEW, safeDataUri)` limpa. Adicione explicitamente `Intent.CATEGORY_BROWSABLE` para garantir que apenas atividades navegáveis tratem a intent, remova componentes perigosos e implemente o tratamento validado de URL de fallback vinculado ao pacote de destino.
Como a visibilidade de pacotes do Android 11 afeta a interceptação de deep link em WebViews?
A visibilidade de pacotes do Android 11 não impede que `startActivity()` inicie atividades externas correspondentes. No entanto, se o seu `WebViewClient` realizar verificações de pré-vôo usando `resolveActivity()` ou `queryIntentActivities()` antes de tentar um lançamento, você deve declarar o nome do pacote de destino ou o esquema de intent personalizado dentro do elemento `<queries>` no seu arquivo `AndroidManifest.xml`.

Resumo e Framework de Decisão

Tratar deep linking e esquemas personalizados dentro de WebViews Android exige preencher a lacuna entre sandboxes de renderização web e roteamento de intent do sistema operacional nativo. Confiar no comportamento padrão da WebView resulta em erros de protocolo não tratados, experiência do usuário ruim e possíveis vulnerabilidades de segurança.

Ao implementar um WebViewClient robusto que verifica fronteiras de frame, valida esquemas personalizados contra listas de permissão explícitas, reconstrói intents ACTION_VIEW seguras e trata fallbacks de loja de maneira graciosa, as equipes de engenharia reduzem falhas no tratamento de esquemas em WebViews e suportam uma navegação mais resiliente. Integrar roteamento confiável do lado do cliente com frameworks abrangentes de atribuição móvel reduz a fricção de roteamento através dos fluxos de atribuição e suporta jornadas de usuário mais consistentes.

Para explorar arquiteturas de deep linking nativo Android e roteamento de parâmetros, consulte a documentação de integração do SDK, faça o download das bibliotecas de cliente no centro de downloads do SDK do OpoInstall, explore a referência de implementação de atribuição móvel ou registre seu aplicativo no console de desenvolvedor do OpoInstall.

Materiais Relacionados

Share this article