@@ -6,28 +6,44 @@ package app.tauri.deep_link
66
77import android.app.Activity
88import android.content.Intent
9- import android.os.Bundle
9+ import android.os.PatternMatcher
1010import android.webkit.WebView
11- import app.tauri.Logger
1211import app.tauri.annotation.InvokeArg
1312import app.tauri.annotation.Command
1413import app.tauri.annotation.TauriPlugin
1514import app.tauri.plugin.Channel
1615import app.tauri.plugin.JSObject
1716import app.tauri.plugin.Plugin
1817import app.tauri.plugin.Invoke
18+ import androidx.core.net.toUri
1919
2020@InvokeArg
2121class SetEventHandlerArgs {
2222 lateinit var handler: Channel
2323}
2424
25+ @InvokeArg
26+ class AssociatedDomain {
27+ var scheme: List <String > = listOf (" https" , " http" )
28+ var host: String? = null
29+ var path: List <String > = listOf ()
30+ var pathPattern: List <String > = listOf ()
31+ var pathPrefix: List <String > = listOf ()
32+ var pathSuffix: List <String > = listOf ()
33+ }
34+
35+ @InvokeArg
36+ class PluginConfig {
37+ var mobile: List <AssociatedDomain > = listOf ()
38+ }
39+
2540@TauriPlugin
2641class DeepLinkPlugin (private val activity : Activity ): Plugin(activity) {
2742 // private val implementation = Example()
2843 private var webView: WebView ? = null
2944 private var currentUrl: String? = null
3045 private var channel: Channel ? = null
46+ private var config: PluginConfig ? = null
3147
3248 companion object {
3349 var instance: DeepLinkPlugin ? = null
@@ -51,27 +67,105 @@ class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
5167
5268 override fun load (webView : WebView ) {
5369 instance = this
70+ config = getConfig(PluginConfig ::class .java)
71+
72+ super .load(webView)
73+ this .webView = webView
5474
5575 val intent = activity.intent
5676
57- if (intent.action == Intent .ACTION_VIEW ) {
58- // TODO: check if it makes sense to split up init url and last url
59- this .currentUrl = intent.data.toString()
60- val event = JSObject ()
61- event.put(" url" , this .currentUrl)
62- this .channel?.send(event)
77+ if (intent.action == Intent .ACTION_VIEW && intent.data != null ) {
78+ val url = intent.data.toString()
79+ if (isDeepLink(url)) {
80+ // TODO: check if it makes sense to split up init url and last url
81+ this .currentUrl = url
82+ val event = JSObject ()
83+ event.put(" url" , this .currentUrl)
84+ this .channel?.send(event)
85+ }
6386 }
64-
65- super .load(webView)
66- this .webView = webView
6787 }
6888
6989 override fun onNewIntent (intent : Intent ) {
70- if (intent.action == Intent .ACTION_VIEW ) {
71- this .currentUrl = intent.data.toString()
72- val event = JSObject ()
73- event.put(" url" , this .currentUrl)
74- this .channel?.send(event)
90+ if (intent.action == Intent .ACTION_VIEW && intent.data != null ) {
91+ val url = intent.data.toString()
92+ if (isDeepLink(url)) {
93+ this .currentUrl = url
94+ val event = JSObject ()
95+ event.put(" url" , this .currentUrl)
96+ this .channel?.send(event)
97+ }
7598 }
7699 }
100+
101+ private fun isDeepLink (url : String ): Boolean {
102+ val config = this .config ? : return false
103+
104+ if (config.mobile.isEmpty()) {
105+ return false
106+ }
107+
108+ val uri = try {
109+ url.toUri()
110+ } catch (_: Exception ) {
111+ // not a URL
112+ return false
113+ }
114+
115+ val scheme = uri.scheme ? : return false
116+ val host = uri.host
117+ val path = uri.path ? : " "
118+
119+ // Check if URL matches any configured mobile deep link
120+ for (domain in config.mobile) {
121+ // Check scheme
122+ if (! domain.scheme.any { it.equals(scheme, ignoreCase = true ) }) {
123+ continue
124+ }
125+
126+ // Check host (if configured)
127+ if (domain.host != null ) {
128+ if (! host.equals(domain.host, ignoreCase = true )) {
129+ continue
130+ }
131+ }
132+
133+ // Check path constraints
134+ // According to Android docs:
135+ // - path: exact match, must begin with /
136+ // - pathPrefix: matches initial part of path
137+ // - pathSuffix: matches ending part, doesn't need to begin with /
138+ // - pathPattern: simple glob pattern (., *, .*)
139+ val pathMatches = when {
140+ // Exact path match (must begin with /)
141+ domain.path.isNotEmpty() && domain.path.any { it == path } -> true
142+ // Path pattern match (simple glob: ., *, .*)
143+ domain.pathPattern.isNotEmpty() && domain.pathPattern.any { pattern ->
144+ try {
145+ PatternMatcher (pattern, PatternMatcher .PATTERN_SIMPLE_GLOB ).match(path)
146+ } catch (e: Exception ) {
147+ false
148+ }
149+ } -> true
150+ // Path prefix match
151+ domain.pathPrefix.isNotEmpty() && domain.pathPrefix.any { prefix ->
152+ path.startsWith(prefix)
153+ } -> true
154+ // Path suffix match
155+ domain.pathSuffix.isNotEmpty() && domain.pathSuffix.any { suffix ->
156+ path.endsWith(suffix)
157+ } -> true
158+ // If no path constraints, any path is allowed
159+ domain.path.isEmpty() && domain.pathPattern.isEmpty() &&
160+ domain.pathPrefix.isEmpty() && domain.pathSuffix.isEmpty() -> true
161+ else -> false
162+ }
163+
164+ if (pathMatches) {
165+ return true
166+ }
167+ }
168+
169+ return false
170+ }
77171}
0 commit comments