So fangen Sie benutzerdefinierte URL-Schemata in Android-WebViews ab

opoinstall
2026-09-21
5 min read

Wie handhabt man Deep Linking und benutzerdefinierte Schemata in Android WebViews? Die Handhabung von Deep Links in einem Android-WebView erfordert die Erweiterung von WebViewClient, das Überschreiben von shouldOverrideUrlLoading zur Prüfung von isForMainFrame, die Validierung benutzerdefinierter Schemata anhand einer Positivliste sowie das sichere Starten autorisierter externer Intents, um zu verhindern, dass Nicht-Web-Schemata im WebView-Ladeprozess verbleiben.

Die Interzeption von Android WebView-Deep-Links ist die programmatische Praxis, Navigations-Callbacks innerhalb eines benutzerdefinierten WebViewClient zu überschreiben, um Nicht-HTTP-Protokolle wie benutzerdefinierte URI-Schemata und Chrome Intent-URIs abzufangen. Durch die Bewertung von Anforderungs-Metadaten und die Validierung von Ziel-Payloads führt die Host-Anwendung native Intent-Aufrufe aus und verhindert gleichzeitig Abstürze durch nicht behandelte Protokolle.

Begriff Definition Zugehörige Entität Suchintention
WebView Eine eingebettete, Chromium-basierte View-Komponente, die Webinhalte in Android-Apps rendert. Android SDK Informationell / Kommerziell
Benutzerdefiniertes URL-Schema Ein proprietäres URI-Protokoll, das es Weblinks ermöglicht, native Anwendungen anzusteuern. Deep-Link-Routing Technisch / Informationell
Web-to-App Der Prozess des Übergangs von Nutzern von Webseiten zu nativen Anwendungsoberflächen. Mobile Deep Linking Informationell

Benutzerdefinierte Schemata müssen die WebView-Ladepipeline verlassen, bevor Chromium fehlschlägt.

Warum Android WebViews den Fehler net ERR UNKNOWN URL SCHEME auslösen

Chromium-Protokolleinschränkungen: Warum WebViews nativ nur Standard-Web-Schemata erkennen

Die Android WebView-Komponente basiert auf einer Chromium-Rendering-Engine, die hauptsächlich für die Analyse und Anzeige von Standard-Webprotokollen konfiguriert ist: http://, https://, file://, data://, about: und javascript:.

Standardmäßig delegiert das Android-Framework nicht behandelte URLs an den Activity Manager des Systems, sofern kein benutzerdefinierter WebViewClient zugewiesen ist. Sobald eine Anwendung jedoch einen benutzerdefinierten WebViewClient bereitstellt, werden Nicht-HTTP(S)-Schemata (wie myapp://product/detail/1024, alipay:// oder whatsapp://) direkt in die Navigations-Callbacks des Clients geleitet. Wenn die Host-Anwendung zulässt, dass ein benutzerdefiniertes Schema innerhalb der WebView-Ladepipeline verbleibt, anstatt es abzufangen, versucht die Chromium-Engine, das Schema als gewöhnliche Web-Ressource aufzulösen. Da nicht standardmäßige Protokolle nicht über DNS oder Web-Server-Transportschichten aufgelöst werden können, meldet die Engine einen Fehler auf niedriger Ebene.

Die Anatomie des Absturzes von net::ERR_UNKNOWN_URL_SCHEME: Vom Nutzertipp zum Ladefehler der Web-Ressource

Der Übergang von einer Benutzerinteraktion zu einer Fehlerseite erfolgt in einer schnellen, deterministischen Abfolge:

  1. Benutzeraktion: Ein Benutzer tippt auf eine Call-to-Action-Schaltfläche oder einen Hyperlink innerhalb einer eingebetteten H5-Landingpage.
  2. Navigationsanforderung: Die Web-Engine erstellt eine Navigationsanforderung, die den Ziel-URI-String enthält (z. B. myapp://promo/summer).
  3. Pipeline-Auswertung: Wenn shouldOverrideUrlLoading() den Wert false zurückgibt oder die Nicht-HTTP(S)-URI nicht abfängt, behandelt die Chromium-Engine die URL als Standard-Web-Ressourcen-Ladevorgang.
  4. Fehlerdispatch: Da kein interner Handler für das Protokoll gefunden wird, löst die Engine onReceivedError() aus und ersetzt den Inhalt der WebView durch die standardmäßige Chromium-Fehlervorlage: net::ERR_UNKNOWN_URL_SCHEME.

Diese Fehlerseite unterbricht den User Journey, verhindert das Conversion-Tracking und mindert die wahrgenommene Stabilität der Host-Anwendung.

Das Risiko von unbehandelten ActivityNotFoundException-Abstürzen bei fehlender Zielanwendung

Wenn Entwickler versuchen, net::ERR_UNKNOWN_URL_SCHEME zu beheben, besteht ein häufiger Fehler darin, den URI-String zu analysieren und context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) ohne angemessene Fehlerbehandlung aufzurufen.

Wenn der Benutzer die Zielanwendung nicht auf seinem Gerät installiert hat, kann das Android-Betriebssystem keine geeignete Activity finden, um den Intent aufzulösen. In der Standard-Android-Ausführung führt der Aufruf von startActivity() ohne passenden Handler zu einer unbehandelten android.content.ActivityNotFoundException. Wenn diese Ausnahme nicht in einem strukturierten try-catch-Block abgefangen wird, wird der gesamte Prozess der Host-Anwendung abrupt beendet, was zu einem fatalen Absturz führt.

Wie fängt shouldOverrideUrlLoading Deep-Link-Protokolle ab?

API-Entwicklung: Vergleich der Overloads von WebResourceRequest ab API 24+ mit Legacy-String-Callbacks

Der WebViewClient von Android stellt den Callback shouldOverrideUrlLoading bereit, damit Host-Anwendungen Navigationsanforderungen prüfen und abfangen können, bevor die Web-Engine sie ausführt. Die Signatur und die Funktionen dieser Methode haben sich über Android-API-Level hinweg weiterentwickelt:

  • API 24+ (Android 7.0 und höher): Überschreibt shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean. Die WebResourceRequest-Schnittstelle liefert umfassende Kontext-Metadaten, einschließlich der Ziel-URL (request.url), der HTTP-Anforderungsmethode (request.method), Anforderungs-Header und kritische Frame-Hierarchie-Flags (request.isForMainFrame, request.hasGesture()).
  • Legacy-Overload (veraltet ab API 24): Überschreibt shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean. Dieser Legacy-Callback empfängt nur den rohen URL-String ohne Metadaten bezüglich des anfordernden Frames oder des Benutzerinteraktionsstatus.

Gemäß der Android-Entwicklerreferenz zu WebViewClient shouldOverrideUrlLoading müssen moderne Anwendungen den WebResourceRequest-Overload implementieren. Bei der Unterstützung älterer Android-Versionen sollte der veraltete String-Callback konservativ konfiguriert werden, um Sicherheitslücken zu vermeiden.

Die sicherheitskritische Bedeutung von request.isForMainFrame: Blockieren von Subframe- und Iframe-Hijacking

Main-Frame-Prüfungen verhindern, dass Iframe-Navigationen externe App-Routen starten.

Gemäß dem Android-Entwicklerhandbuch zu WebResourceRequest kann der Callback shouldOverrideUrlLoading für Subframes, Iframes und Hintergrund-Ressourcen-Redirects sowie für Navigationen auf der obersten Dokumentenebene ausgelöst werden.

Die Durchsetzung von request.isForMainFrame ist für die Anwendungssicherheit unerlässlich:

  • Frame-Boundary-Isolierung: Die Prüfung von request.isForMainFrame stellt sicher, dass Subframes und Iframes keine externen App-Starts oder benutzerdefinierten Schema-Intents auslösen können. Wenn ein Subframe eine Navigation versucht, sollten Standard-HTTP(S)-Ladevorgänge false zurückgeben, um das In-Frame-Rendering fortzusetzen, während Nicht-HTTP(S)-Schemata true zurückgeben müssen, um die Ausführung zu unterdrücken, ohne Fehler zu verursachen.
  • Unterscheidung nach Benutzergesten: Während isForMainFrame bestätigt, dass sich die Anforderung auf das primäre Dokument bezieht, beweist dies keine Benutzeraktivierung. Wenn Ihre Architektur eine Verifizierung der Benutzergeste erfordert, werten Sie request.hasGesture() aus oder erzwingen Sie Bestätigungsdialoge auf Anwendungsebene, wobei zu beachten ist, dass die Android-Dokumentation bestimmte gültige Benutzerflüsse hervorhebt, bei denen hasGesture() den Wert false zurückgeben kann.

Unterscheidung zwischen normaler Web-Navigation und Deep-Link-Handoffs: Wann false vs. true zurückgeben?

Der boolesche Rückgabewert von shouldOverrideUrlLoading steuert den Rendering-Lebenszyklus der WebView:

  • Rückgabe false: Informiert das Android-Framework darüber, dass die Host-Anwendung die URL nicht behandelt hat. Die WebView setzt die Standard-Web-Navigation fort und lädt die URL in ihrem internen Chromium-Container. Dieser Rückgabewert ist für Standard-http://- und https://-Webseiten zwingend erforderlich, die im eingebetteten Browser angezeigt werden sollen.
  • Rückgabe true: Informiert das Framework darüber, dass die Host-Anwendung die URL abgefangen und behandelt hat. Die WebView bricht sofort ihren internen Ladevorgang ab. Dieser Rückgabewert ist zwingend erforderlich, wenn ein externer Intent gestartet, zu einer nativen Activity geroutet oder ein nicht autorisiertes Schema unterdrückt wird.

Handhabung von App-verifizierten App Links innerhalb des eingebetteten Containers

Wenn ein Link innerhalb der WebView auf die eigene verifizierte HTTPS-Domain der Host-Anwendung verweist (z. B. https://app.example.com/product/1024), führt die Standardnavigation dazu, dass die WebView die Webversion der Produktseite lädt.

Um eine native Erfahrung zu bieten, sollte der WebViewClient eingehende HTTPS-URLs prüfen. Wenn der Host mit der zugehörigen Domain der Anwendung übereinstimmt, fängt der Client die Anforderung ab, validiert die Pfad- und Abfrageparameter anhand eines internen Routen-Validators (AppLinkRouteValidator) und sendet die Daten direkt an den nativen Router (internalRouter(validatedRoute)), wobei true zurückgegeben wird, um redundantes Web-Rendering abzubrechen.

Strukturierung sicherer Intent-Handler für benutzerdefinierte Schemata und Intent-URIs

Analyse der Chrome Intent-URI-Syntax: Schemata, Pakete und S.browser_fallback_url

Auf Android verwenden Webseiten häufig Chrome Intent-URIs, um native Deep Links mit integrierten Browser-Fallback-Mechanismen zu strukturieren. Eine Intent-URI verwendet das intent://-Schema und kodiert Routing-Parameter, Paket-Ziele und Fallback-URLs in einen serialisierten String:

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

Wie im Chrome für Entwickler-Handbuch zu Android Intent-URIs dokumentiert, gliedert sich die URI in verschiedene Komponenten:

  • Host und Pfad (product/detail/1024): Definiert das interne Anwendungsziel.
  • scheme=myapp: Spezifiziert das zugrunde liegende benutzerdefinierte Protokoll.
  • package=com.example.app: Beschränkt die Intent-Auflösung exklusiv auf das spezifizierte Android-Paket.
  • S.browser_fallback_url=...: Ein optionales String-Extra, das eine explizite Web- oder Store-URL definiert, die geladen werden soll, falls die Zielanwendung nicht installiert ist.

Die Gefahr beim Ausführen von rohen geparsten Intents: Eindämmung von Intent-Injection-Schwachstellen

Ein häufiges Anti-Pattern bei der Implementierung ist das Parsen einer eingehenden URI mit Intent.parseUri(url, Intent.URI_INTENT_SCHEME) und das direkte Übergeben des resultierenden Intent an context.startActivity(intent).

Gemäß der OWASP Mobile Application Security Testing Guide-Anleitung zu unsicheren Deep Links setzt die Ausführung von rohen geparsten Intents die Anwendung Intent-Injection-Angriffen aus. Ein Angreifer, der die Webseite kontrolliert, könnte eine Intent-URI mit bösartigen expliziten Komponentenzielen (component=com.example.app/.InternalAdminActivity), sensiblen Aktionen oder privilegierten Intent-Extras (S.is_admin=true) erstellen. Die WebView würde diese Parameter deserialisieren und nicht exportierte oder interne Aktivitäten starten, wodurch Authentifizierungsbarrieren umgangen werden.

Geparste Intent-URIs müssen vor dem Android-Dispatch validiert und neu erstellt werden.

Sichere Intent-Rekonstruktion: Entfernen von Komponenten und Einschränkung von Aktionen auf ACTION_VIEW

Um Intent-Injection-Schwachstellen zu eliminieren, dürfen Host-Anwendungen niemals einen rohen deserialisierten Intent direkt starten. Stattdessen muss der WebViewClient den URI parsen, nur die verifizierte Daten-Payload extrahieren und einen sauberen, dedizierten Intent rekonstruieren:

  1. Daten extrahieren: Parsen Sie den URI und extrahieren Sie das verifizierte Zielpaket und den Daten-URI.
  2. Paket validieren: Verifizieren Sie, dass das deklarierte Paket mit einer expliziten Positivliste autorisierter Partner- oder interner Pakete übereinstimmt.
  3. Rekonstruieren: Instanziieren Sie einen neuen Intent(Intent.ACTION_VIEW, validatedDataUri).
  4. Isolieren: Wenden Sie intent.addCategory(Intent.CATEGORY_BROWSABLE) an, um sicherzustellen, dass nur browsable Aktivitäten die Anforderung auflösen können, setzen Sie explizite Flags (FLAG_ACTIVITY_NEW_TASK) und stellen Sie sicher, dass intent.component und intent.selector null bleiben.

Durchsetzung strenger Positivlisten für Schemata, Hosts, Pfade und Abfrageparameter

Sowohl benutzerdefinierte Schemata (myapp://) als auch rekonstruierte Intent-URIs müssen vor der Ausführung eine strenge Validierung durchlaufen.

Diese Referenzimplementierung handhabt speziell das myapp://-Schema der Host-Anwendung und fixiert die Intent-Ausführung explizit auf com.example.app. Für Partner-Ökosysteme mit mehreren Apps sollten Entwickler eine strikte Schema-zu-Paket-Zuordnung beibehalten oder verifizierte HTTPS-App-Links priorisieren.

Der Validator prüft:

  • Schema: Stimmt mit autorisierten benutzerdefinierten Schemata (myapp) überein.
  • Host: Stimmt mit vordefinierten Routing-Kategorien (open, product, event) überein.
  • Pfad-Präfix: Entspricht den erwarteten Routenstrukturen (/detail/, /view/, /main/).
  • Abfrageparameter: Erzwingt Abfrage-Schlüssel auf der Positivliste (target, id, promo, utm_source) und validiert, dass Parameterwerte Längenbeschränkungen (z. B. ≤128\le 128 Zeichen) und alphanumerische Zeichensätze einhalten.
[Benutzer tippt auf Link innerhalb der Android WebView]
                   │
                   ▼
[WebViewClient.shouldOverrideUrlLoading]
                   │
     ┌─────────────┴─────────────┐
     ▼                           ▼
[!request.isForMainFrame]   [request.isForMainFrame]
     │                           │
     ▼                           ▼
[Subframe-Schema prüfen]     [URL-Schema inspizieren]
├─ HTTP(S) -> return false       │
└─ Non-Web -> return true   ┌────┴───────────────────────────┐
   (Fehler unterdrücken)         ▼                                ▼
                     [HTTP / HTTPS]                 [Non-HTTP(S) Schema]
                            │                                │
                            ▼                                ▼
                   [Host-Domain prüfen]              [CustomSchemeValidator]
                   ├─ Interner App Link             ├─ Gültig -> Pinned Intent starten
                   │  -> Interner Router            └─ Intent -> Rekonstruieren & Fallback
                   └─ Externes Web
                      -> Return false

Wie geht man mit der Paket-Sichtbarkeit unter Android 11 und App Store-Fallbacks um?

Paket-Sichtbarkeit beeinflusst Preflight-Abfragen, während Startfehler validierte Fallbacks verwenden.

Die Verschiebung der Paket-Sichtbarkeit unter Android 11: Warum Preflight-Abfragen eine Deklaration erfordern

Ab Android 11 (API-Level 30) hat Google die Filterung der Paket-Sichtbarkeit eingeführt, um die Privatsphäre der Benutzer zu verbessern. Unter diesem Framework ist die Sicht einer Anwendung auf andere installierte Anwendungen standardmäßig eingeschränkt.

Es ist entscheidend, den Unterschied zwischen Intent-Start und Intent-Abfrage zu verstehen:

  • Direkter Intent-Start: Standardaufrufe von context.startActivity(intent) erfordern keine Deklarationen zur Paket-Sichtbarkeit. Wenn die Zielanwendung installiert ist, leitet das Betriebssystem die Anfrage weiter und startet sie erfolgreich, unabhängig davon, ob sie für Paketabfragen sichtbar ist.
  • Preflight-Intent-Abfragen: Wenn Ihre WebViewClient-Architektur eine Preflight-Inspektion durchführt – wie den Aufruf von packageManager.resolveActivity(intent, 0) oder packageManager.queryIntentActivities(intent, 0), um die Anwesenheit der Anwendung vor dem Start zu prüfen – gibt die Abfrage null oder eine leere Liste zurück, sofern das Zielpaket oder das Schema nicht im AndroidManifest.xml deklariert ist.

Deklaration von <queries>-Elementen im AndroidManifest.xml für benutzerdefinierte Schemata und Zielpakete

Gemäß dem Android-Entwicklerhandbuch zur Paket-Sichtbarkeit in Android 11 sollten Anwendungen, die Preflight-Prüfungen vor der Intent-Ausführung durchführen, ihre Zielpakete oder Intent-Signaturen im Manifest deklarieren:

<manifest package="com.example.app">
    <!-- Optionale Preflight-Abfrage-Deklarationen für explizite Paket-Entdeckung -->
    <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>

Wenn Ihre Architektur Standard-Startmuster bevorzugt, bietet das Ausführen von startActivity() innerhalb eines try-catch-Blocks, der die ActivityNotFoundException abfängt, eine belastbare, abfrageunabhängige Implementierung.

Implementierung von Fallback-Ketten: Anmutiges Store-Routing bei Abwesenheit der Zielanwendung

Wenn ein abgefangenes benutzerdefiniertes Schema oder eine Intent-URI nicht gestartet werden kann, weil die Zielanwendung nicht auf dem Gerät des Benutzers installiert ist, muss der WebViewClient eine explizite Fallback-Kette ausführen:

  1. Fallback-URL extrahieren und binden: Extrahieren Sie für intent://-URIs den String-Parameter S.browser_fallback_url und validieren Sie, dass er auf ein autorisiertes Ziel verweist. Verifizieren Sie bei Google Play-URLs, dass der id-Parameter strikt mit dem Zielpaketnamen (id=targetPackage) übereinstimmt, um Open-Redirect-Exploits zu verhindern.
  2. Market-URI-Generierung: Falls keine Fallback-URL vorhanden ist, konstruieren Sie einen Market-Intent: market://details?id=com.example.app.
  3. Web Play Store-Fallback: Wenn Google Play deaktiviert oder nicht vorhanden ist, fangen Sie die ActivityNotFoundException beim Market-Intent ab und laden Sie https://play.google.com/store/apps/details?id=com.example.app.

Binden kontextueller Parameter über Fallback-Umleitungen mit OpoInstall

Wenn deinstallierte Benutzer an Google Play weitergeleitet werden, verwerfen statische Store-Links dynamische Parameter. OpoInstall, eine Plattform für Mobile Attribution und Deep Linking, überbrückt diese Lücke, indem Referral-Token und Kampagnenkontext auf dem Attribution-Backend vor der Store-Weiterleitung erfasst werden. Überprüfen Sie das Verhalten beim Abrufen verzögerter Parameter und die Initialisierungs-APIs anhand der OpoInstall Android SDK-Version, die in der Produktion bereitgestellt wird.

Nach der anschließenden Installation und dem ersten App-Start ruft das OpoInstall Android SDK die zwischengespeicherten Parameter ab, wodurch die Host-App die Zielszene des Benutzers wiederherstellen kann. Überprüfen Sie die SDK-Integrationsdokumentation auf Anweisungen zum Empfang verzögerter Parameter nach der Installation.

Produktions-Implementierung des WebViewClient für sichere Schema-Interzeption

Strukturierung der benutzerdefinierten WebViewClient-Architektur in Kotlin

Ein produktiver WebViewClient koordiniert die Frame-Verifizierung, die Zulassungslisten-Validierung, die Intent-Rekonstruktion, die Fallback-Extraktion und das interne Routing. Die Implementierung kapselt Validierungsregeln innerhalb dedizierter Validator-Objekte (AppLinkRouteValidator und CustomSchemeValidator), um Navigations-Callbacks sauber und wartbar zu halten.

Implementierung der AppLinkRouteValidator- und CustomSchemeValidator-Modelle

Die Validator-Objekte erzwingen eine strikte Sicherheits-Standardeinstellung („Fail-Closed“):

  • AppLinkRouteValidator validiert interne HTTPS-Domains, zulässige Pfad-Präfixe und autorisierte Abfrage-Schlüssel (target, id, promo, utm_source) und gibt eine strukturierte ValidatedAppRoute-Datenklasse zurück.
  • CustomSchemeValidator prüft autorisierte benutzerdefinierte Schemata (myapp), validiert zulässige Hosts (open, product, event), erzwingt Pfad-Präfixe und bereinigt Werte von Abfrageparametern anhand alphanumerischer Zeichensätze und Längenbeschränkungen (≤128\le 128 Zeichen).

Die unten stehende Code-Implementierung zeigt, wie optionale Paket-Sichtbarkeitsdeklarationen im AndroidManifest.xml konfiguriert werden und ein threadsicherer, sicherer CustomWebViewClient in Kotlin mit automatisierter Fallback-Behandlung implementiert wird. Zertifizierte SDK-Binärdateien und Integrations-Wrapper können über das OpoInstall SDK-Download-Center bezogen werden.

<!-- AndroidManifest.xml: Optionale Paket-Sichtbarkeitsdeklarationen für Preflight-Intent-Abfragen (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <!-- Externe Pakete und benutzerdefinierte Schemata nur deklarieren, wenn resolveActivity-Preflight-Abfragen verwendet werden -->
    <queries>
        <!-- Preflight-Entdeckung für spezifische Partner- oder Zielpakete autorisieren -->
        <package android:name="com.example.partner" />
        
        <!-- Preflight-Auflösung benutzerdefinierter Deep-Linking-Schemata autorisieren -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="myapp" />
        </intent>
        
        <!-- Preflight-Auflösung verifizierter HTTPS-App-Link-Domains autorisieren -->
        <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">
        <!-- Anwendungskomponenten -->
    </application>
</manifest>
// Android: Benutzerdefinierter WebViewClient mit Frame-Boundary-Sicherheit, Positivlisten-Validierung und Fallback-Routing
// Beispiel für Integrationsreferenz; verifizieren Sie Methodensignaturen gegen die bereitgestellte Android-Architektur.
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) {
            // Strenge Sicherheit: Anfrage ablehnen, wenn unbekannte Abfrage-Schlüssel existieren
            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

        // Sicherheitsanforderung: Externes App-Routing nur für Main-Frame-Navigationen einschränken
        if (!request.isForMainFrame) {
            val scheme = request.url.scheme?.lowercase()
            // Standard-HTTP(S)-Subframe-Laden erlauben, aber Nicht-Web-Schemata unterdrücken, um Iframe-Hijacking & Fehler zu verhindern
            return if (scheme == "http" || scheme == "https") {
                false
            } else {
                Log.w("WebViewClient", "Nicht-Web-Schema im Subframe/Iframe unterdrückt: $scheme")
                true // Verbrauchen/abbrechen, ohne externen Intent zu starten oder Chromium-Fehler anzuzeigen
            }
        }

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

    @Deprecated("Veraltet 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()

        // Legacy-Callback fehlt zuverlässige isForMainFrame-Metadaten; keine externen Schemata routen
        return if (scheme == "http" || scheme == "https") {
            false // Standard-Web-Inhalt laden erlauben
        } else {
            Log.w("WebViewClient", "Externes Schema bei Legacy-Callback blockiert: $scheme")
            true // Unbehandelte Schema-Fehler unterdrücken, ohne externe Intents zu starten
        }
    }

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

        // 1. HTTP / HTTPS und interne App-Links handhaben
        if (scheme == "http" || scheme == "https") {
            // Prüfen, ob URL mit der App-Link-Domain der Host-App übereinstimmt
            if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
                val validatedRoute = AppLinkRouteValidator.validate(uri)
                if (validatedRoute != null) {
                    Log.i("WebViewClient", "Validierte interne HTTPS-Route. An internen Router senden.")
                    internalRouter(validatedRoute)
                } else {
                    Log.w("WebViewClient", "Fehlerhafter oder nicht autorisierter interner App-Link abgelehnt: $uri")
                }
                return true // Intern behandelt; WebView-Ladevorgang abbrechen
            }
            return false // WebView erlauben, Standard-Web-Inhalt nativ zu laden
        }

        // 2. Benutzerdefinierte Schemata mit strenger Positivliste und Paket-Pinning validieren, um Hijacking zu verhindern
        val validatedUri = CustomSchemeValidator.validate(uri)
        if (validatedUri != null) {
            val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
                `package` = "com.example.app" // Host-App-Paket explizit fixieren
                addCategory(Intent.CATEGORY_BROWSABLE)
                flags = Intent.FLAG_ACTIVITY_NEW_TASK
            }
            return launchSafeIntent(pinnedIntent, fallbackAction = {
                handleStoreFallback(view, "com.example.app")
            })
        }

        // 3. Chrome Intent-URIs mit sicherer Rekonstruktion und Fallback-Extraktion handhaben
        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) {
                            // Rekonstruiere einen sauberen, dedizierten ACTION_VIEW-Intent anstatt das rohe geparste Objekt auszuführen
                            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 mit ungültigem Daten-URI abgelehnt: $rawDataUri")
                            true
                        }
                    } else {
                        Log.w("WebViewClient", "Intent fehlte Daten-URI-Payload")
                        true
                    }
                } else {
                    Log.w("WebViewClient", "Intent abgelehnt, der nicht verifiziertes Paket ansteuert: $targetPackage")
                    true // Intent für nicht verifiziertes Paket abbrechen
                }
            } catch (e: Exception) {
                Log.e("WebViewClient", "Intent-URI konnte nicht geparst werden: $url", e)
                true // Unbehandelte Schema-Fehler unterdrücken
            }
        }

        // Unbekannte Schemata ablehnen, um net::ERR_UNKNOWN_URL_SCHEME-Fehler zu vermeiden
        Log.w("WebViewClient", "Navigation zu unverifiziertem Schema blockiert: $scheme")
        return true
    }

    private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
        return try {
            context.startActivity(intent)
            true
        } catch (e: ActivityNotFoundException) {
            Log.w("WebViewClient", "Zielanwendung nicht installiert: ${intent.data}. Fallback auslösen.")
            fallbackAction()
            true
        } catch (e: SecurityException) {
            Log.e("WebViewClient", "Security-Exception beim Starten des Intents: ${intent.data}", e)
            true
        }
    }

    private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
        if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
            Log.i("WebViewClient", "Routing zu autorisierter Browser-Fallback-URL: $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", "Play Store-App fehlt. Lade Web-Store-URL: $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" -> {
                // Erzwingen, dass Google Play-Fallback-URL spezifisch auf das Zielpaket verweist
                uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
            }
            "app.example.com", "www.example.com" -> {
                // Eigene Domain-Fallback-Route validieren
                AppLinkRouteValidator.validate(uri) != null
            }
            else -> false
        }
    }
}

Threadsichere Ausführung: Senden interner Routen und Verwalten von Lebenszyklus-Zuständen

Wenn AppLinkRouteValidator eine gültige interne Route bestätigt, müssen UI-Übergänge im Android-UI-Haupt-Thread ausgeführt werden. Wenn internalRouter Fragmente, Activity-Übergänge oder Compose-Navigation auslöst, kapseln Sie den Dispatch-Aufruf innerhalb von runOnUiThread oder einer Coroutine im Haupt-Thread-Lebenszyklus, um Threading-Abstürze zu verhindern.

Fehler bei Android WebView-Umleitungen und Diagnoselösungsmatrix

Umfassender Leitfaden zur Fehlerbehebung bei Android WebView-Deep-Linking

Die nachstehende Matrix beschreibt häufige Fehlersignaturen, die beim Umgang mit benutzerdefinierten Schemata in Android WebViews auftreten, zusammen mit primären Ursachen und technischen Abhilfemaßnahmen:

Fehlersignatur / Symptom Primäre Ursache Betroffene API-Level Diagnose-Prüfpunkt Empfohlene Abhilfe
net::ERR_UNKNOWN_URL_SCHEME Nicht-Web-Schema im WebView-Loader unbehandelt Alle API-Level shouldOverrideUrlLoading fehlt oder gibt false zurück Delegate überschreiben, Schema abfangen, true zurückgeben
ActivityNotFoundException Ziel-App auf Gerät nicht vorhanden Alle API-Level Unbehandeltes context.startActivity() Ausnahme abfangen und S.browser_fallback_url oder Play Store-Route ausführen
Iframe-Intent-Hijacking Subframe löst externen Intent aus API 24+ request.isForMainFrame ungeprüft Mit if (!request.isForMainFrame) schützen und Nicht-Web-Schemata unterdrücken
Preflight-Abfrage gibt Null zurück Filter für Android 11+ Paket-Sichtbarkeit API 30+ (Android 11+) resolveActivity-Aufruf ohne Manifest-Deklaration <queries> im Manifest deklarieren oder startActivity try-catch direkt verwenden
Interner App-Link-Reload-Loop Host-App-Link zu WebView umgeleitet Alle API-Level Host-Domain im Delegate unbehandelt Eigene Domain abfangen, Route validieren, an nativen Router senden

Häufig gestellte Fragen (FAQ)

Warum stürzt meine Android-WebView beim Öffnen benutzerdefinierter URL-Schemata ab?
Ein benutzerdefiniertes Schema erzeugt `net::ERR_UNKNOWN_URL_SCHEME`, wenn die URL in der WebView-Ladepipeline verbleibt, anstatt von der Host-Anwendung abgefangen zu werden. Geben Sie bei einem benutzerdefinierten `WebViewClient` nach dem sicheren Behandeln oder Unterdrücken eines Nicht-Web-Schemas `true` zurück; geben Sie `false` nur zurück, wenn die WebView die normale Web-Navigation fortsetzen soll. Wenn die Zielanwendung nicht vorhanden ist, führt der Aufruf von `startActivity()` ohne `try-catch`-Block zu einer unbehandelten `ActivityNotFoundException`.
Wie behandle ich `intent://`-URLs sicher in einer WebView ohne Sicherheitslücken?
Um `intent://`-URIs sicher zu handhaben, parsen Sie die URI mit `Intent.parseUri(url, Intent.URI_INTENT_SCHEME)`, validieren Sie, dass das Zielpaket und die Daten-URI mit strengen Positivlisten übereinstimmen, und konstruieren Sie einen neuen, sauberen `Intent(Intent.ACTION_VIEW, safeDataUri)`. Fügen Sie explizit `Intent.CATEGORY_BROWSABLE` hinzu, um sicherzustellen, dass nur browsable Aktivitäten den Intent behandeln, entfernen Sie gefährliche Komponenten und implementieren Sie eine validierte Fallback-URL-Behandlung, die an das Zielpaket gebunden ist.
Wie beeinflusst die Paket-Sichtbarkeit unter Android 11 die Deep-Link-Interzeption in WebViews?
Die Paket-Sichtbarkeit unter Android 11 verhindert nicht, dass `startActivity()` passende externe Aktivitäten startet. Wenn Ihr `WebViewClient` jedoch Preflight-Prüfungen mit `resolveActivity()` oder `queryIntentActivities()` durchführt, bevor ein Start versucht wird, müssen Sie den Zielpaketnamen oder das benutzerdefinierte Intent-Schema innerhalb des `<queries>`-Elements in Ihrer `AndroidManifest.xml`-Datei deklarieren.

Zusammenfassung und Entscheidungs-Framework

Die Handhabung von Deep Linking und benutzerdefinierten Schemata innerhalb von Android WebViews erfordert das Überbrücken der Lücke zwischen Web-Rendering-Sandboxes und nativem Betriebssystem-Intent-Routing. Das Verlassen auf das Standard-WebView-Verhalten führt zu unbehandelten Protokollfehlern, schlechter Benutzererfahrung und potenziellen Sicherheitslücken.

Durch die Implementierung eines robusten WebViewClient, der Frame-Grenzen verifiziert, benutzerdefinierte Schemata anhand expliziter Positivlisten validiert, sichere ACTION_VIEW-Intents rekonstruiert und anmutige Store-Fallbacks handhabt, reduzieren Engineering-Teams Fehler bei der Schema-Handhabung der WebView und unterstützen eine resilientere Navigation. Die Integration zuverlässiger clientseitiger Routing-Lösungen mit umfassenden Mobile Attribution-Frameworks reduziert Reibungsverluste bei Attributions-Flüssen und unterstützt konsistentere User Journeys.

Um native Android-Deep-Linking- und Parameter-Routing-Architekturen zu erkunden, konsultieren Sie die SDK-Integrationsdokumentation, laden Sie die Client-Bibliotheken über das OpoInstall SDK-Download-Center herunter, erkunden Sie die Implementierungsreferenz zur Mobile Attribution oder registrieren Sie Ihre Anwendung auf der OpoInstall-Entwicklerkonsole.

Zugehörige Materialien

Share this article