Comment lier des paramètres personnalisés lors de la création de codes QR ? La liaison de paramètres personnalisés pour le routage d'applications nécessite l'ajout de clés de requête d'URL structurées à un lien de page de destination hébergé, la capture de ces clés via une couche d'intégration Web et l'envoi de la charge utile validée aux contrôleurs de navigation natifs lors du réveil direct de l'application ou du lancement post-installation.
Le routage in-app via des codes QR est une architecture de navigation client-serveur qui intègre des paramètres de destination structurés dans l'URL d'un code-barres bidimensionnel. En résolvant ces paramètres par le biais de liens profonds du système d'exploitation lorsque l'application est installée, ou par une récupération différée prise en charge par la plateforme lorsque l'application est nouvellement installée à partir d'une boutique d'applications, les équipes d'ingénierie peuvent acheminer les utilisateurs directement vers des écrans in-app spécifiques plutôt que vers la vue d'accueil par défaut.
| Terme | Définition | Entité associée | Rôle de l'intention de recherche |
|---|---|---|---|
| Créer un code QR pour l'application | Le flux de travail d'ingénierie consistant à encoder des URL de routage exploitables dans des codes-barres 2D pour la navigation mobile. | Restauration de scène | Informationnel / Technique |
| code qr d'application | Un actif de code-barres configuré pour déclencher le lancement d'une application native ou l'intégration en magasin. | Liens universels / Liens d'application | Technique / Commercial |
| code qr dynamique | Un code-barres pointant vers un point de terminaison de routage intermédiaire dont la charge utile de destination peut être mise à jour côté serveur. | Indirection URI | Commercial |
| suivi de code qr | La mesure des requêtes d'entrée de routage et des activations d'écrans in-app en aval. | Statistiques des canaux et suivi | Commercial |
Aperçu de l'architecture : Comment créer un code QR pour la restauration de scènes d'application

Le problème de l'abandon sur l'écran d'accueil : Pourquoi les redirections génériques en magasin brisent les parcours utilisateur à forte intention
Les codes QR physiques sont fréquemment déployés dans des environnements contextuels à forte intention : un acheteur scanne une étiquette de rayon pour afficher les spécifications d'un produit, un client de restaurant scanne un chevalet de table pour commander à partir d'une catégorie de menu spécifique, ou un joueur scanne un code-barres d'invitation pour rejoindre un salon de jeu multijoueur. Lorsque le code-barres encode un lien de boutique d'applications générique ou un domaine non paramétré, l'intention contextuelle de l'utilisateur est perdue lors de la transition du scan physique vers l'exécution de l'application native.
Lors du lancement de l'application, l'utilisateur est renvoyé vers l'écran d'accueil supérieur par défaut. Forcer l'utilisateur à naviguer manuellement dans des arborescences de catégories complexes, à saisir à nouveau des requêtes de recherche ou à taper des identifiants de salle introduit une friction cognitive et mécanique substantielle. La restauration de scènes in-app résout cet abandon en encodant l'état de destination dans l'URL du code QR et en reconstruisant par programmation cette vue cible à l'intérieur du client natif.
Mécaniques de routage à double état : Réveil direct vs. Restauration de scène différée

Lorsqu'un utilisateur scanne un code QR de routage d'application, l'appareil mobile se trouve dans l'un des deux états d'installation possibles. Une architecture de routage complète gère les deux cycles de vie par le biais de chemins techniques distincts :
- Routage par réveil direct (Application déjà installée) : Lorsque l'application cible est déjà présente sur l'appareil, le scan du code QR déclenche un lien profond du système d'exploitation — plus précisément les Liens universels Apple sur iOS ou les Liens d'application Android sur Android. Les deux mécanismes nécessitent l'hébergement de fichiers d'association de domaines vérifiés cryptographiquement : iOS requiert un fichier
apple-app-site-association(AASA) associé à une habilitationAssociated Domainsd'Xcode, tandis qu'Android requiert un fichier de liaison d'actifs numériques/.well-known/assetlinks.jsoncorrespondant à l'empreinte SHA-256 du certificat de signature de l'application et à des filtres d'intention de manifeste vérifiés. Une fois vérifié, le système d'exploitation réveille l'application et transmet l'URI directement au délégué de cycle de vie du client. - Restauration de scène différée (Application non installée) : Lorsque l'application est absente, l'utilisateur est acheminé via une page de destination Web intermédiaire vers la boutique d'applications de la plateforme. Le binaire natif n'étant pas encore installé pour recevoir une intention du système d'exploitation, les paramètres de routage doivent être préparés avant la redirection vers la boutique et récupérés lors du lancement initial de l'application (démarrage à froid au Jour 0) une fois l'installation terminée.
Limites de récupération spécifiques à la plateforme : Google Play Install Referrer, liens universels iOS et adaptateurs SDK
Une fois qu'un utilisateur installe une application à partir d'une boutique d'applications, la récupération des paramètres de parrainage et de routage dépend de la plateforme et du mécanisme mis en œuvre par l'application ou le SDK d'attribution. Transmettre un paramètre route ou id via une URL Web ne garantit pas, en soi, que le paramètre survivra à une installation depuis une boutique d'applications :
- Android et Google Play: L'API Google Play Install Referrer peut fournir des métadonnées de parrainage prises en charge associées à une installation depuis le Play Store. Ce mécanisme s'applique spécifiquement aux installations complétées via le chemin officiel du Google Play Store, et non aux chargements latéraux d'APK tiers arbitraires ou aux boutiques alternatives. L'application doit configurer et tester la charge utile de parrainage réelle et vérifier que les paramètres attendus survivent au flux de redirection et d'installation.
- iOS et l'App Store: L'App Store d'Apple ne transfère pas automatiquement les paramètres de requête de page de destination Web arbitraires dans un binaire d'application nouvellement installé. Les liens universels prennent en charge le routage direct lorsque l'application est déjà installée et que la configuration du domaine associé est valide ; ils ne garantissent pas, à eux seuls, la récupération différée des paramètres après une nouvelle installation.
- SDK d'attribution: Utilisez uniquement les mécanismes de récupération documentés pour la version exacte du SDK et la plateforme en cours de déploiement. Vérifiez l'implémentation, la collecte de données et les implications en matière de confidentialité plutôt que de supposer qu'un SDK peut récupérer chaque paramètre de routage à travers chaque navigateur et chemin d'installation.
Attribution iOS et limites de confidentialité : Conformité aux règles anti-empreintes d'Apple
Les implémentations de routage iOS et de restauration de scènes différées ne doivent pas s'appuyer sur le profilage d'appareils (fingerprinting) interdit pour identifier un utilisateur ou un appareil à travers des applications ou des sites Web. En vertu des directives d'Apple sur la confidentialité des utilisateurs et l'utilisation des données et du contrat de licence du programme de développement Apple, les développeurs ont strictement interdiction de dériver un identifiant unique à partir de signaux relatifs à l'appareil, au navigateur, à la configuration, à l'emplacement ou au réseau à cette fin. L'autorisation App Tracking Transparency (ATT) ne rend pas autorisée une méthode de profilage par ailleurs interdite.
Utilisez les liens universels pour le routage direct d'applications vérifié lorsque l'application est installée. Pour la restauration de scènes post-installation sur iOS, utilisez un mécanisme documenté dont le comportement a été examiné pour la version du SDK déployée et les exigences applicables d'Apple. Examinez les divulgations de confidentialité du SDK et les déclarations d'API à raison obligatoire le cas échéant, mais ne traitez pas la présence d'un manifeste de confidentialité (PrivacyInfo.xcprivacy) comme la preuve qu'une implémentation d'attribution est conforme.
Si le SDK sélectionné repose sur une méthode de correspondance qui ne peut pas être vérifiée comme autorisée, ne recommandez pas cette méthode pour un déploiement iOS. Lorsqu'aucun mécanisme autorisé ne peut établir de manière fiable le contexte de routage différé, préservez le parcours d'intégration normal, amenez l'utilisateur sur une vue d'accueil par défaut sécurisée et marquez l'installation comme non attribuée plutôt que de fabriquer une correspondance non vérifiée.
Visualisation du pipeline de restauration de scène QR à double état
Le diagramme d'architecture ci-dessous illustre les étapes suivantes où un code QR paramétré se ramifie en un réveil direct du système d'exploitation ou en une récupération différée en magasin avant de converger vers le validateur de route côté client et la barrière d'authentification :
[User Scans QR Code] ──> URL (?route=product_detail&id=sku_8842&channelCode=pos_01)
│
┌────┴────────────────────────────────────────┐
▼ (App Already Installed) ▼ (App Not Installed)
[OS Universal Link / App Link] [Intermediary H5 Landing Page]
│ │
▼ ▼
[Hot/Cold Wakeup Callback] [Store Download & First Launch]
(onNewIntent / continueUserActivity) (Platform-Supported Deferred Recovery)
│ │
└────────────────────┬────────────────────────┘
▼
[Client Route Validator & Auth Gate]
│
┌────────────────────┴────────────────────────┐
▼ (Allowlisted & Authenticated) ▼ (Invalid Route / Unmatched)
[Navigate to Product Detail Screen] [Fallback to Default Home Screen]
Étape 1 : Conception du schéma de paramètres de routage personnalisés
Structuration de charges utiles de routage exploitables vs. Balises d'attribution passive
Lors de la création d'un code QR pour le routage d'applications, les équipes d'ingénierie doivent établir un contrat de données clair à deux niveaux qui sépare les clés de requête d'URL compactes externes de la charge utile JSON normalisée interne :
| Niveau de contrat | Convention de nommage des champs | Représentation d'exemple | Objectif architectural |
|---|---|---|---|
| Niveau de requête d'URL externe | route, id, channelCode |
?route=product_detail&id=sku_8842&channelCode=pos_01 |
Minimise la longueur des caractères de l'URI pour garder la matrice du code QR imprimé compacte |
| Niveau normalisé interne | route_key, target_id, channel_code, fallback_route |
{"route_key": "product_detail", "target_id": "sku_8842", ...} |
Objet canonique standardisé validé par les routeurs H5 et clients natifs |
| Registre de sécurité client | PROTECTED_ROUTES (Constante interne) |
setOf("room_lobby", "promo_campaign") |
Détermine les exigences d'authentification en interne ; jamais approuvé à partir d'URL externes |

Dans ce modèle à deux niveaux :
- Balises d'attribution passive (
channelCodechannel_code) sont consommées par les pipelines d'analyse pour mesurer le volume de conversion marketing sans modifier l'état de l'interface utilisateur du client. - Paramètres de routage exploitables (
routeroute_keyetidtarget_id) indiquent au contrôleur de navigation du client natif quel écran monter et quelle entité récupérer. - Isolation de la politique d'authentification : Le fait qu'une route nécessite une authentification (
requires_auth) doit être dérivé strictement du registre de routes de confiance interne de l'application cliente plutôt qu'accepté à partir d'un paramètre de requête de code QR externe.
Application de l'encodage en pourcentage IETF RFC 3986 et des contraintes de taille de charge utile
Tous les paramètres de routage ajoutés à l'URL de destination du code QR doivent adhérer à la syntaxe générique d'URI spécifiée dans la norme IETF RFC 3986 :
- Éviter les délimiteurs imbriqués : N'intégrez pas de chaînes JSON brutes (
?data={"route":"product","id":123}) directement dans les chaînes de requête d'URL. Les accolades, guillemets et deux-points non échappés brisent fréquemment les analyseurs d'URL des navigateurs in-app et gonflent la densité des modules de codes-barres. - Aplatir les paires clé-valeur : Représentez le contexte de routage externe sous forme de paramètres de requête plats et encodés en pourcentage (
https://promo.example.com/open?route=product_detail&id=sku_8842&channelCode=pos_01). - Appliquer des limites de caractères strictes : Restreignez les valeurs
routeetidaux caractères alphanumériques sécurisés pour les URL, aux traits d'union et aux underscores (^[A-Za-z0-9_-]{1,64}$).
Définition du contrat de liste blanche de routes côté client
Une fois que les paramètres d'URL externes sont ingérés et normalisés dans la structure de charge utile interne, ils doivent se conformer à un contrat de schéma strict. Si route_key n'est pas reconnu, ou si une route spécifique à une entité (product_detail, room_lobby, promo_campaign) ne dispose pas de son target_id requis, le normaliseur doit rétrograder la charge utile vers home_default avec target_id: null.
Le schéma JSON ci-dessous définit la structure canonique pour une charge utile de restauration de scène normalisée :
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "QrSceneRestorationPayload",
"description": "Canonical, normalized routing payload. Authentication policy is derived separately from the application's trusted route registry.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"route_key",
"target_id",
"fallback_route"
],
"properties": {
"schema_version": {
"const": "1.0.0"
},
"route_key": {
"type": "string",
"enum": [
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]
},
"target_id": {
"type": ["string", "null"],
"pattern": "^[A-Za-z0-9_-]{1,64}$"
},
"channel_code": {
"type": "string",
"pattern": "^[A-Za-z0-9_.-]{1,64}$"
},
"fallback_route": {
"const": "home_default"
}
},
"allOf": [
{
"if": {
"properties": {
"route_key": {
"enum": [
"product_detail",
"room_lobby",
"promo_campaign"
]
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "string"
}
}
}
},
{
"if": {
"properties": {
"route_key": {
"const": "home_default"
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "null"
}
}
}
}
],
"examples": [
{
"schema_version": "1.0.0",
"route_key": "product_detail",
"target_id": "sku_8842",
"channel_code": "pos_01",
"fallback_route": "home_default"
}
]
}
Étape 2 : Capture et transmission du contexte de routage sur la page de destination
Analyse des clés de routage autorisées sur la page de destination H5
Lorsqu'un utilisateur ne disposant pas de l'application installée scanne le code QR, le navigateur mobile ouvre la page de destination H5 intermédiaire hébergée. Le script de la page de destination extrait les clés de requête externes (route, id, channelCode), rejette les paramètres en double ambigus et les normalise dans la structure canonique définie à l'Étape 1.
Le normaliseur JavaScript ci-dessous applique la cardinalité à clé unique, les limites d'expressions régulières et la rétrogradation conditionnelle vers home_default tout en traitant les paramètres réservés en double comme non valides :
const query = new URLSearchParams(window.location.search);
const routeValues = query.getAll("route");
const idValues = query.getAll("id");
const channelValues = query.getAll("channelCode");
const allowedRoutes = new Set([
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]);
const idPattern = /^[A-Za-z0-9_-]{1,64}$/;
const channelPattern = /^[A-Za-z0-9_.-]{1,64}$/;
function normalizeQrRoutingPayload() {
// Reject duplicate keys to defend against parameter pollution
if (routeValues.length > 1 || idValues.length > 1 || channelValues.length > 1) {
return {
schema_version: "1.0.0",
route_key: "home_default",
target_id: null,
fallback_route: "home_default"
};
}
const rawRoute = routeValues.length === 1 ? routeValues[0] : null;
const rawId = idValues.length === 1 ? idValues[0] : null;
const rawChannel = channelValues.length === 1 ? channelValues[0] : null;
let routeKey = allowedRoutes.has(rawRoute)
? rawRoute
: "home_default";
let targetId =
rawId && idPattern.test(rawId)
? rawId
: null;
if (routeKey === "home_default" || targetId === null) {
routeKey = "home_default";
targetId = null;
}
const payload = {
schema_version: "1.0.0",
route_key: routeKey,
target_id: targetId,
fallback_route: "home_default"
};
if (rawChannel && channelPattern.test(rawChannel)) {
payload.channel_code = rawChannel;
}
return payload;
}
const routingPayload = normalizeQrRoutingPayload();
Intégration du SDK Web via un adaptateur à version vérifiée
La page de destination coordonne à la fois les tentatives de réveil immédiat de l'application et la mise en scène des paramètres différés. Avant de déployer un SDK Web d'attribution (tel que opoinstall.js), confirmez l'URL exacte du bundle JavaScript, la signature d'initialisation, le rappel de disponibilité, la méthode de réveil et le schéma de paramètres par rapport à la documentation officielle de la version du SDK déployée.
Conservez le comportement spécifique au SDK derrière un petit adaptateur d'application qui accepte l'objet routingPayload normalisé. Pour des spécifications détaillées sur l'intégration Web et la configuration des paramètres de canal, consultez la documentation développeur OpoInstall.
Préservation des paramètres de routage via des liens de secours directs vers la boutique
Si le SDK Web JS n'est pas disponible ou est bloqué par les contrôles de sécurité du navigateur, la page de destination bascule vers un lien de redirection direct vers la boutique. Sur les déploiements Android utilisant Google Play, le lien de secours peut encoder les clés de routage normalisées dans le paramètre referrer du Play Store afin que le client natif puisse récupérer le contexte de routage via l'API Google Play Install Referrer lorsqu'il est installé via le flux officiel du Play Store :
// Example Android Play Store fallback URL preserving normalized routing context
const referrerString = `route_key=${routingPayload.route_key}&target_id=${routingPayload.target_id || ""}&channel_code=${routingPayload.channel_code || ""}`;
const playStoreFallbackUrl = `https://play.google.com/store/apps/details?id=com.example.app&referrer=${encodeURIComponent(referrerString)}`;
Étape 3 : Implémentation de la distribution de routes pour client natif et de la restauration de scènes
Gestion des modes de lancement d'activité Android : Pourquoi singleTask nécessite à la fois onCreate et onNewIntent
Dans les architectures de liens profonds Android, la façon dont une activité reçoit les intentions de routage dépend de son mode de lancement configuré et de la conception de son graphe de navigation. Les développeurs peuvent utiliser des modes de lancement standard avec Jetpack Navigation, singleTop ou singleTask en fonction de leurs exigences de pile de tâches.
Lorsqu'une activité est configurée avec singleTop ou singleTask pour empêcher les instances en double de s'empiler lors des réveils par liens profonds, deux chemins de cycle de vie distincts doivent être gérés :
- Démarrage à froid (
onCreate): Lorsque le processus de l'application n'est pas en cours d'exécution, le scan du code QR lance l'activité à partir de zéro. L'objetIntententrant est transmis àonCreate(savedInstanceState: Bundle?). - Démarrage à chaud (
onNewIntent): Lorsque le système d'exploitation réutilise une instance d'activité existante selon le mode de lancement configuré et les conditions de pile de tâches, l'Intention de lien profond entrante est transmise àonNewIntent(). SoussingleTop, la réutilisation ne se produit que lorsque l'activité cible est déjà au sommet de la pile de tâches pertinente ; sinon, une nouvelle instance d'activité peut être créée. Implémentez la gestion deonCreate()et deonNewIntent(), et vérifiez le comportement réel pour l'architecture de navigation de l'application.
Pour gérer les deux états de manière fiable sans bloquer définitivement un utilisateur qui scanne légitimement le même code QR une seconde fois plus tard dans sa session, l'activité doit marquer chaque instance d'Intent traitée avec un fanion de consommation (EXTRA_ROUTE_CONSUMED) et ignorer les intentions réacheminées uniquement par les lancements historiques du sélecteur d'applications (Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY).
Gestion des points d'entrée du cycle de vie iOS : AppDelegate vs. Activités de continuation UISceneDelegate
Sur iOS, le routage direct par lien universel et la récupération de paramètres différés doivent s'aligner sur le cycle de vie de l'interface utilisateur configuré de l'application :
- Cycle de vie basé sur les scènes (
UISceneDelegate): Dans les applications adoptantUIScene(standard à partir d'iOS 13), les liens universels lancés à froid arrivent dansscene(_:willConnectTo:options:)viaconnectionOptions.userActivities, tandis que les liens universels repris à chaud arrivent dansscene(_:continue:). - Cycle de vie du délégué d'application (
UIApplicationDelegate): Dans les applications qui n'utilisent pas de délégués de scène, les liens universels sont transmis àapplication(_:continue:restorationHandler:). - Récupération différée au premier lancement: Indépendamment des délégués de continuation de liens universels, la requête de paramètres d'installation différée s'exécute une fois lors du démarrage initial de l'application après que la hiérarchie des vues racine est prête.
Les développeurs peuvent télécharger des packages de SDK de plate-forme et inspecter les modèles d'intégration du cycle de vie via le centre de téléchargement du SDK OpoInstall.
Unification des paramètres de réveil direct et des paramètres différés de premier lancement en un seul routeur de navigation
Pour maintenir un comportement de sécurité et de navigation cohérent, les architectures clientes normalisent à la fois les URI de réveil direct et les charges utiles JSON d'installation différée en un seul modèle NavigationTarget avant de les transmettre au routeur d'interface utilisateur :
- Vérification de l'origine (Intentions directes): Vérifiez que les URI
Intent.ACTION_VIEWentrantes utilisent le schémahttps, correspondent à l'hôte associé vérifié de l'application (promo.example.com) et au chemin (/open), et ne contiennent aucune clé de requête en double conflictuelle. - Normalisation du schéma et vérification de la liste blanche: Mappez les clés externes
route/idou les champs internesroute_key/target_iddans le modèle canonique, en rejetant les routes non autorisées ou les ID malformés. Notez que la logique d'analyse Kotlin du client fonctionne comme un squelette d'analyse défensif illustratif ; dans les systèmes de production, analysez les charges utiles par rapport à l'ensemble des règles de schémas canoniques avant la distribution. - Garde de réentrance à vol unique: Utilisez une garde atomique par distribution afin que les rappels simultanés ne puissent pas déclencher de transactions de fragments qui se chevauchent.
Gestion des barrières d'authentification : Mise en file d'attente des routes de liens profonds en attente jusqu'à la fin de la connexion de l'utilisateur

Lorsqu'un nouvel utilisateur installe une application à partir d'une boutique d'applications via un code QR et la lance pour la première fois, il n'est généralement pas authentifié. Si le code QR cible un écran protégé (tel que room_lobby ou promo_campaign) et que le routeur tente de naviguer immédiatement, la barrière d'authentification redirigera l'utilisateur vers l'écran de connexion et perdra le contexte du lien profond.
Pour préserver la restauration de scènes à travers les barrières d'authentification :
- Recherche de politique de route interne: Déterminez si la route nécessite une authentification en vérifiant le registre interne
PROTECTED_ROUTESdu client plutôt qu'en faisant confiance aux fanions de charge utile externe. - Mise en scène atomique: Si l'utilisateur n'est pas authentifié, stockez le
NavigationTargetvalidé dans unPendingRouteStoreatomique à portée de session (AtomicReference) et acheminez l'utilisateur vers le flux de connexion. Notez qu'unPendingRouteStoreen mémoire s'applique au processus d'application actif ; si le flux d'authentification peut s'étendre sur la mort du processus, utilisez un magasin local chiffré et limité dans le temps qui efface l'état lors du changement de compte. - Ré-évaluation et distribution post-authentification: Lorsque la connexion est établie, consommez atomiquement la cible mise en scène (
getAndSet(null)), vérifiez que la session est maintenant authentifiée, re-validez les paramètres de route et exécutez la transition d'écran.
Disponibilité de l'hôte de navigation et restauration d'état
Le rappel onNavigationGraphReady() doit être connecté à l'initialisation de l'hôte de navigation réel de l'application. Lorsque le graphe de navigation est défini dans un NavHostFragment hébergé par XML, l'activité peut établir cette connexion une fois que l'hôte et sa destination sont prêts. Si le graphe est installé de manière asynchrone, invoquez le rappel uniquement une fois que l'initialisation du graphe est terminée.
Une demande de navigation en attente stockée uniquement dans un champ d'activité peut être perdue lors des changements de configuration. Pour les implémentations en production, conservez l'état de routage non consommé dans un ViewModel conscient du cycle de vie et utilisez SavedStateHandle lorsque la restauration lors d'une recréation d'activité pilotée par le système ou d'une mort de processus est requise. Le code présenté ici est un exemple architectural et nécessite une navigation et une intégration de session spécifiques à l'application.
Le code Kotlin ci-dessous fournit un squelette de routage Android illustratif avec des adaptateurs de navigation et de SDK appartenant à l'application démontrant ces contrôles :
<!-- Android Manifest Configuration for App Link Verification -->
<!-- File: app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".SceneRestorationActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- Verified Android App Link Intent Filter -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Origin must match /.well-known/assetlinks.json SHA-256 fingerprint -->
<data
android:scheme="https"
android:host="promo.example.com"
android:pathPrefix="/open" />
</intent-filter>
</activity>
</application>
</manifest>
// Android Routing Activity Implementation
// File: app/src/main/java/com/example/app/SceneRestorationActivity.kt
//
// INTEGRATION NOTE:
// This example is an architectural routing skeleton, not a drop-in production implementation.
// Bind AttributionRoutingAdapter and NavController initialization to your verified SDK release,
// session manager, and NavHostFragment before deployment.
package com.example.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import org.json.JSONObject
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* Validated navigation target model representing an allowlisted in-app destination.
* Authentication requirement is derived strictly from the internal route registry.
*/
data class NavigationTarget(
val eventKey: String,
val routeKey: String,
val targetId: String?,
val requiresAuth: Boolean
)
/**
* Process-scoped atomic store holding a pending route when an unauthenticated user
* triggers a protected deep link during cold start or first-launch onboarding.
*/
object PendingRouteStore {
private val pending = AtomicReference<NavigationTarget?>(null)
fun stage(target: NavigationTarget) {
pending.set(target)
}
fun consume(): NavigationTarget? {
return pending.getAndSet(null)
}
}
class SceneRestorationActivity : AppCompatActivity() {
companion object {
private const val TAG = "SceneRestorationRouter"
private const val TRUSTED_HOST = "promo.example.com"
private const val TRUSTED_PATH = "/open"
private const val EXTRA_ROUTE_CONSUMED = "com.example.app.EXTRA_ROUTE_CONSUMED"
private const val ROUTE_HOME = "home_default"
private const val ROUTE_PRODUCT = "product_detail"
private const val ROUTE_LOBBY = "room_lobby"
private const val ROUTE_PROMO = "promo_campaign"
private val ALLOWED_ROUTES = setOf(ROUTE_HOME, ROUTE_PRODUCT, ROUTE_LOBBY, ROUTE_PROMO)
private val PROTECTED_ROUTES = setOf(ROUTE_LOBBY, ROUTE_PROMO)
private val SAFE_ID_REGEX = Regex("^[A-Za-z0-9_-]{1,64}$")
// Process-level guard ensuring deferred install recovery is queried only once per cold start
private val hasQueriedDeferredInstall = AtomicBoolean(false)
}
// Per-delivery re-entrancy guard; allows legitimate subsequent scans of the same QR code
private val processingIntent = AtomicBoolean(false)
// Staging container for routes that arrive before the UI/NavController is mounted
private val pendingUiTarget = AtomicReference<NavigationTarget?>(null)
private var navigationReady = false
// Application-owned adapter isolating vendor SDK and session calls
private val attributionAdapter = AttributionRoutingAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scene_restoration)
// Connect NavHost initialization when defined in layout
connectNavigationHost()
// Activity recreation (e.g., screen rotation) is not a new install or new QR scan
if (savedInstanceState != null) {
return
}
val launchIntent = intent
val isDirectLinkLaunch =
launchIntent?.action == Intent.ACTION_VIEW &&
launchIntent.data != null
if (isDirectLinkLaunch) {
processIncomingIntentOnce(launchIntent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
// Do not start deferred recovery after a direct-link launch,
// even if the direct route was rejected.
return
}
// Only initial normal application launches may request deferred recovery
if (hasQueriedDeferredInstall.compareAndSet(false, true)) {
queryDeferredSceneParameters()
}
}
/**
* Connect this method to the actual NavHost initialization.
* Uses currentDestination check and destination listener to safely verify graph readiness
* without throwing IllegalStateException on unset controller.graph.
*/
private fun connectNavigationHost() {
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
if (navHost == null) {
Log.w(TAG, "NavHost not attached; route remains queued.")
// For an asynchronously attached NavHost, call this
// method again after the host has been attached.
return
}
val controller = navHost.navController
// currentDestination is nullable before a destination has been established.
// Do not access controller.graph merely to check whether setGraph() has been called.
if (controller.currentDestination != null) {
onNavigationGraphReady()
return
}
// If the graph/destination becomes available later,
// mark navigation ready when a destination is established.
val listener = object : androidx.navigation.NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
navController: androidx.navigation.NavController,
destination: androidx.navigation.NavDestination,
arguments: Bundle?
) {
navController.removeOnDestinationChangedListener(this)
onNavigationGraphReady()
}
}
controller.addOnDestinationChangedListener(listener)
}
/**
* Captures warm-start QR scans when the Activity is reused under singleTop or singleTask.
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
processIncomingIntentOnce(intent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
}
private fun processIncomingIntentOnce(incomingIntent: Intent?, process: (Intent) -> Unit) {
val candidate = incomingIntent ?: return
// Ignore historical intents re-delivered from recent-apps switcher or configuration changes
if ((candidate.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) return
if (candidate.getBooleanExtra(EXTRA_ROUTE_CONSUMED, false)) return
if (!processingIntent.compareAndSet(false, true)) return
try {
process(candidate)
candidate.putExtra(EXTRA_ROUTE_CONSUMED, true)
} finally {
processingIntent.set(false)
}
}
private fun handleIncomingWakeupIntent(incomingIntent: Intent?): Boolean {
if (incomingIntent?.action != Intent.ACTION_VIEW) return false
val uri: Uri = incomingIntent.data ?: return false
// 1. Verify trusted HTTPS origin and path
val isTrusted =
uri.scheme.equals("https", ignoreCase = true) &&
uri.host.equals(TRUSTED_HOST, ignoreCase = true) &&
uri.path == TRUSTED_PATH
if (!isTrusted) {
Log.w(TAG, "Rejected untrusted deep-link origin: $uri")
return false
}
// 2. Reject missing or conflicting duplicate query parameters
if (uri.getQueryParameters("route").size != 1 ||
uri.getQueryParameters("id").size != 1) {
Log.w(TAG, "Rejected missing or duplicate routing keys in URI")
return false
}
val rawRoute = uri.getQueryParameter("route")
val rawId = uri.getQueryParameter("id")
val target = validateAndBuildTarget("wakeup", rawRoute, rawId)
if (target.routeKey != ROUTE_HOME) {
dispatchNavigationTarget(target)
return true
}
return false
}
private fun queryDeferredSceneParameters() {
// Application-defined timeout configuration; tune for deployed service behavior
attributionAdapter.fetchDeferredInstallParams(timeoutMs = 5000L) { jsonPayload ->
if (jsonPayload == null) {
Log.d(TAG, "No deferred routing parameters recovered; remaining on default view.")
return@fetchDeferredInstallParams
}
val target = parseJsonRoutingPayload("deferred_install", jsonPayload)
runOnUiThread {
dispatchNavigationTarget(target)
}
}
}
private fun parseJsonRoutingPayload(sourcePrefix: String, rawJson: String): NavigationTarget {
return try {
val obj = JSONObject(rawJson)
// Defensive normalization: map canonical 'route_key'/'target_id' with external 'route'/'id' fallback
val rawRoute = obj.optString("route_key", "").ifEmpty { obj.optString("route", "") }
val rawId = if (obj.has("target_id") && !obj.isNull("target_id")) {
obj.optString("target_id", "")
} else {
obj.optString("id", "")
}
validateAndBuildTarget(sourcePrefix, rawRoute, rawId)
} catch (e: Exception) {
Log.w(TAG, "Malformed routing JSON payload: ${e.message}")
NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
}
/**
* Enforces strict allowlist and regex bounds on untrusted routing parameters.
*/
private fun validateAndBuildTarget(
sourcePrefix: String,
rawRoute: String?,
rawId: String?
): NavigationTarget {
val cleanRoute = if (rawRoute != null && ALLOWED_ROUTES.contains(rawRoute)) {
rawRoute
} else {
ROUTE_HOME
}
if (cleanRoute == ROUTE_HOME) {
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
// Entity-specific routes strictly require a valid, sanitized targetId
val cleanId = if (rawId != null && SAFE_ID_REGEX.matches(rawId)) {
rawId
} else {
null
}
if (cleanId == null) {
Log.w(TAG, "Route $cleanRoute rejected due to missing or invalid target_id.")
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
val needsAuth = PROTECTED_ROUTES.contains(cleanRoute)
return NavigationTarget("${sourcePrefix}_${cleanRoute}_${cleanId}", cleanRoute, cleanId, needsAuth)
}
/**
* Invoked when the application's NavHost / NavController graph is attached and ready.
*/
fun onNavigationGraphReady() {
navigationReady = true
flushPendingNavigation()
}
private fun flushPendingNavigation() {
if (!navigationReady || isFinishing || isDestroyed) return
val target = pendingUiTarget.getAndSet(null) ?: return
dispatchReadyNavigationTarget(target)
}
private fun dispatchNavigationTarget(target: NavigationTarget) {
if (isFinishing || isDestroyed) return
// Stage route if the UI navigation graph has not finished inflating
if (!navigationReady) {
pendingUiTarget.set(target)
return
}
dispatchReadyNavigationTarget(target)
}
private fun dispatchReadyNavigationTarget(target: NavigationTarget) {
if (target.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
// Check authentication gate for protected destinations
if (target.requiresAuth && !attributionAdapter.isUserAuthenticated()) {
Log.d(TAG, "Protected route ${target.routeKey} staged pending user authentication.")
PendingRouteStore.stage(target)
navigateToLoginScreen()
return
}
executeScreenTransition(target.routeKey, target.targetId)
}
/**
* Invoked by the authentication controller after login or registration completes.
*/
fun onUserAuthenticationCompleted() {
val pending = PendingRouteStore.consume()
if (pending == null) {
navigateToHomeScreen()
return
}
if (isFinishing || isDestroyed) return
if (!attributionAdapter.isUserAuthenticated()) {
PendingRouteStore.stage(pending)
navigateToLoginScreen()
return
}
val validated = validateAndBuildTarget(
pending.eventKey,
pending.routeKey,
pending.targetId
)
if (validated.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
executeScreenTransition(validated.routeKey, validated.targetId)
}
private fun executeScreenTransition(routeKey: String, targetId: String?) {
// Implementation: perform backend-authorized resource fetch and invoke NavController.navigate()
Log.d(TAG, "Dispatching verified screen transition: route=$routeKey, id=$targetId")
}
private fun navigateToLoginScreen() {
// Implementation: transition to LoginFragment / LoginActivity
}
private fun navigateToHomeScreen() {
// Implementation: display default home screen
}
}
/**
* Application-owned adapter skeleton. Replace stubbed methods with verified OpoInstall SDK
* callbacks, Google Play Install Referrer queries, and your production session manager.
*/
class AttributionRoutingAdapter {
fun fetchDeferredInstallParams(timeoutMs: Long, callback: (String?) -> Unit) {
// Bind to verified SDK install parameter API or Play Install Referrer in production
callback(null)
}
fun isUserAuthenticated(): Boolean {
// Bind to production authentication session state
return false
}
}
Sécurité et diagnostics : Prévention de l'injection de routes et des échecs de navigation
Défense contre l'injection de routes de liens profonds et les exploits de redirection ouverte selon les directives de l'OWASP
Les paramètres extraits des codes QR ou des rappels d'attribution différée représentent des entrées externes non sécurisées. Conformément à la norme OWASP Mobile Application Security Verification Standard et aux directives et risques de sécurité du Top 10 Mobile de l'OWASP, les routeurs clients doivent appliquer trois limites défensives :
- Interdiction du chargement d'URL WebView arbitraires: Ne permettez jamais à un paramètre de routage (
?route=webview&url=https://...) de charger une URL externe non validée à l'intérieur d'une WebView d'application interne. Des attaquants peuvent encoder des URL de hameçonnage dans un code QR, exploitant le cadre de l'interface utilisateur native de l'application pour récolter des identifiants. - Rejet de la pollution de paramètres en double: Si une URL manipulée contient plusieurs clés conflictuelles (
?route=home_default&route=room_lobby), rejetez l'URI au lieu de permettre à l'analyseur de choisir un premier ou un dernier élément arbitraire. - Application de l'autorisation des ressources côté serveur: Transmettre
target_id=sku_8842indique uniquement au client quelle vue demander. Lorsque l'écran interroge l'API du backend, le serveur doit vérifier indépendamment que la session utilisateur actuelle est autorisée à accéder à cette entité, ce qui prévient les vulnérabilités de référence directe d'objet non sécurisée (IDOR).
Diagnostic des conditions de course de navigation au démarrage à chaud vs. démarrage à froid
Lors d'un démarrage à froid, les intentions de liens profonds ou les rappels de SDK mis en cache peuvent se résoudre avant que le NavHostFragment principal ou le graphe de navigation racine n'ait fini de se gonfler. Tenter d'appeler navController.navigate() avant que la hiérarchie des vues ne soit prête provoque des exceptions d'exécution ou des transitions abandonnées. Différez toujours l'exécution de la navigation jusqu'à ce que la hiérarchie des vues hôtes soit attachée et active (!isFinishing && !isDestroyed).
Gestion élégante des promotions expirées, des SKU supprimés et des paramètres manquants
Les codes QR physiques restent souvent déployés dans les magasins de détail ou les médias imprimés après la fin d'une promotion numérique ou le retrait d'un SKU de produit. Lorsque l'écran cible récupère des données pour target_id et reçoit une réponse HTTP 404 Not Found ou 410 Gone de la part du backend, le client doit intercepter l'erreur, afficher un avis d'information clair à l'utilisateur et naviguer proprement vers home_default.
Liste de contrôle de vérification pré-déploiement pour le routage QR à double état
Avant de publier des supports QR physiques, les équipes d'ingénierie doivent exécuter le protocole de vérification en sept points suivant sur des appareils physiques :
- Étape 1 : Réveil au démarrage à froid avec application installée: Forcez la fermeture de l'application installée et scannez le code QR. Confirmez que le système d'exploitation ouvre l'application directement dans l'écran cible (
product_detail). - Étape 2 : Réveil au démarrage à chaud avec application installée et nouveau scan: L'application étant ouverte sur un onglet secondaire, placez l'application en arrière-plan et scannez le code QR. Confirmez que
onNewIntentachemine vers l'écran cible. Naviguez ailleurs, scannez exactement le même code QR une seconde fois et confirmez qu'il navigue à nouveau au lieu d'être bloqué par une déduplication trop large. - Étape 3 : Récupération différée sans application installée (Route publique): Désinstallez l'application, scannez le code QR, terminez l'installation depuis la boutique et lancez. Confirmez que le rappel de paramètre différé normalise la charge utile et achemine vers
product_detail. - Étape 4 : Récupération différée sans application installée avec barrière d'authentification (Route protégée): Désinstallez l'application, scannez un code QR ciblant
room_lobby, installez et lancez en étant déconnecté. Confirmez que l'application s'achemine d'abord vers la connexion et, une fois la connexion terminée, consommePendingRouteStoreet navigue versroom_lobby. - Étape 5 : Rejet d'origine non fiable et de clé en double: Déclenchez une intention
ACTION_VIEWà partir d'un hôte non fiable ou avec des paramètres de requête en double (?route=product_detail&route=room_lobby). Confirmez quehandleIncomingWakeupIntentrejette l'intention et enregistre un avertissement. - Étape 6 : Idempotence de la rotation de l'écran: Après être arrivé sur un écran lié en profondeur, faites pivoter l'appareil pour déclencher la recréation de l'activité. Confirmez que le fanion
EXTRA_ROUTE_CONSUMEDempêche l'intention d'origine de s'exécuter à nouveau.
Analyse comparative des mécanismes de routage in-app
Évaluation des schémas d'URL, des liens universels, des liens d'application et de la restauration de scènes différées
Les plateformes mobiles fournissent des mécanismes distincts pour acheminer les utilisateurs vers les vues d'applications à travers différents états d'installation :
| Mécanisme de routage | État de l'application cible | Exigence de vérification du système d'exploitation | Survie des paramètres lors d'une installation en magasin | Contrainte d'ingénierie principale |
|---|---|---|---|---|
| Schéma d'URL personnalisé | Installée uniquement | Aucune (Déclaration de manifeste uniquement) | Non (Échoue si l'application n'est pas installée) | Déclenche des fenêtres contextuelles d'erreur de navigateur en cas d'absence ; vulnérable au détournement de schéma |
| Liens universels / Liens d'application | Installée principale | iOS : Fichier AASA + habilitation Associated Domains ; Android : assetlinks.json + certificat de signature d'application correspondant et configuration de filtre d'intention vérifiée |
Non (Nécessite une couche différée pour les nouvelles installations) | Peut être supprimé par certains conteneurs webview in-app restreints |
| Google Play Install Referrer | Nouvelles installations Android | Chemin de distribution officiel du Google Play Store | Oui (Chaîne de parrainage prise en charge) | Spécifique au Google Play Store Android ; ne couvre pas iOS ou les boutiques autres que Play |
| Restauration de scène assistée par SDK | Installée & Nouvelles installations | Association de domaine + adaptateur SDK vérifié | Conditionnel au mécanisme de plateforme pris en charge | Nécessite une validation de liste blanche de routes & un examen de la confidentialité de la plateforme |
Compromis architecturaux entre les états d'installation et les systèmes d'exploitation
Les schémas d'URL personnalisés (myapp://product/123) manquent de vérification de la propriété de domaine cryptographique, ce qui signifie que n'importe quelle application peut enregistrer le même schéma et intercepter les liens. De plus, le scan d'un code QR de schéma personnalisé brut lorsque l'application n'est pas installée entraîne une erreur de navigateur irrécupérable.
Les liens universels et les liens d'application Android vérifient la propriété du domaine via HTTPS et reviennent à une page de destination Web lorsque l'application est absente, ce qui en fait la base requise pour le routage de codes QR. La combinaison des liens universels et des liens d'application pour le réveil des applications installées avec une récupération différée prise en charge par la plateforme (telle que Google Play Install Referrer sur Android et des adaptateurs SDK examinés du point de vue de la confidentialité) permet une restauration de scène à double état à partir d'un seul actif de code QR.
Foire aux questions (FAQ)
Comment les paramètres QR personnalisés ouvrent-ils une page in-app spécifique si l'application n'est pas encore installée ?
Pourquoi le scan de mon code QR ouvre-t-il l'application sur l'écran d'accueil au lieu de la page cible ?
Comment puis-je empêcher les codes QR malveillants d'acheminer les utilisateurs vers des écrans internes non autorisés ?
Résumé et cadre de décision
Pour créer des codes QR de routage d'applications avec des paramètres personnalisés, les équipes d'ingénierie doivent relier des chaînes de requête d'URL externes compactes à des contrôleurs de navigation natifs conscients de l'état. Une implémentation complète gère à la fois le réveil direct du système d'exploitation pour les utilisateurs existants et la récupération conditionnelle des paramètres différés pour les nouvelles installations à partir d'une boutique d'applications.
La continuité des paramètres à travers les boutiques d'applications dépendant de mécanismes spécifiques à la plateforme — tels que Google Play Install Referrer sur Android et des flux de travail vérifiés et conformes à la confidentialité sur iOS — les applications doivent traiter les charges utiles de routage récupérées comme des candidats non sécurisés. En combinant des disjoncteurs de route figurant sur liste blanche, la mise en file d'attente de l'état d'authentification et des retours gracieux vers l'écran d'accueil, les équipes peuvent fournir une restauration de scène fiable sans compromettre la stabilité de l'application ou les règles de confidentialité de la plateforme.
Les SDK et la documentation développeur d'OpoInstall fournissent des composants pour l'implémentation du routage web-to-app et de la transmission de paramètres. Confirmez les signatures d'API spécifiques, les schémas de rappel et les comportements de plateforme pris en charge par votre version de SDK déployée avant le déploiement en production.
Pour les détails d'intégration, consultez la documentation développeur OpoInstall, accédez au centre de téléchargement du SDK OpoInstall, ou configurez vos domaines de routage d'applications dans la console développeur OpoInstall.
Matériaux associés
-
Concepts: Restauration de scène, Deep Linking à double état, Liste blanche de routes, Récupération de paramètres différée
-
Technologies: Liens universels, Liens d'application Android, Google Play Install Referrer, Contrôleurs de navigation natifs
-
Normes: Syntaxe générique des identifiants de ressources uniformes (URI) IETF RFC 3986, Norme de vérification de la sécurité des applications mobiles OWASP, Directives et risques de sécurité du Top 10 Mobile de l'OWASP
-
Interfaces d'intégration: Validateur de route de lien profond appartenant à l'application, rappels documentés de réveil et de paramètres d'installation pour la version du SDK déployée et contrôleurs de navigation natifs de la plateforme.
-
Documentation officielle & Références:
Share this article



