Repository navigation
Runtime link handling and custom deep links #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fdb2c18
88de2ab
1137a81
d097846
a69e1f9
a82b86b
53558c3
57454e0
84e8692
28e887e
aa970df
553e22b
a03a410
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package de.starlightunit.wrapper.navigation; | ||
|
|
||
| import java.net.URI; | ||
| import java.net.URISyntaxException; | ||
| import java.net.URLDecoder; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Locale; | ||
|
|
||
| public final class DeepLinkResolver { | ||
| private DeepLinkResolver() { | ||
| } | ||
|
|
||
| public static String resolve( | ||
| String rawUri, | ||
| String configuredScheme, | ||
| String startUrl, | ||
| NavigationPolicy navigationPolicy | ||
| ) { | ||
| String scheme = normalizeScheme(configuredScheme); | ||
| if (scheme.isEmpty() || rawUri == null || rawUri.trim().isEmpty()) { | ||
| return ""; | ||
| } | ||
|
|
||
| final URI incoming; | ||
| final URI base; | ||
| try { | ||
| incoming = new URI(rawUri.trim()); | ||
| base = new URI(startUrl); | ||
| } catch (URISyntaxException | IllegalArgumentException ignored) { | ||
| return ""; | ||
| } | ||
|
|
||
| if (!scheme.equals(normalizeScheme(incoming.getScheme()))) { | ||
| return ""; | ||
| } | ||
|
|
||
| String queryTarget = queryParameter(incoming.getRawQuery(), "url"); | ||
| if (!queryTarget.isEmpty()) { | ||
| return navigationPolicy.isTrustedHttps(queryTarget) ? queryTarget : ""; | ||
| } | ||
|
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Any custom deep link containing a nonempty query parameter named Useful? React with 👍 / 👎. |
||
|
|
||
| String path = incoming.getRawPath(); | ||
| if (path == null || path.isEmpty()) { | ||
| path = "/"; | ||
| } | ||
|
|
||
| String host = incoming.getHost(); | ||
| if (host != null && !host.isEmpty() && !"open".equalsIgnoreCase(host)) { | ||
| path = "/" + host + (path.startsWith("/") ? path : "/" + path); | ||
| } | ||
|
|
||
| String query = incoming.getRawQuery(); | ||
| String fragment = incoming.getRawFragment(); | ||
| StringBuilder relative = new StringBuilder(path.startsWith("/") ? path : "/" + path); | ||
| if (query != null && !query.isEmpty()) { | ||
| relative.append('?').append(query); | ||
| } | ||
| if (fragment != null && !fragment.isEmpty()) { | ||
| relative.append('#').append(fragment); | ||
| } | ||
|
|
||
| try { | ||
| String resolved = base.resolve(relative.toString()).toString(); | ||
| return navigationPolicy.isTrustedHttps(resolved) ? resolved : ""; | ||
| } catch (IllegalArgumentException ignored) { | ||
| return ""; | ||
| } | ||
| } | ||
|
|
||
| public static String normalizeScheme(String value) { | ||
| String normalized = value == null ? "" : value.trim().toLowerCase(Locale.ROOT); | ||
| return normalized.matches("^[a-z][a-z0-9+.-]{1,31}$") ? normalized : ""; | ||
|
Comment on lines
+70
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A profile using a valid one-character URI scheme such as Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private static String queryParameter(String rawQuery, String key) { | ||
| if (rawQuery == null || rawQuery.isEmpty()) { | ||
| return ""; | ||
| } | ||
| for (String pair : rawQuery.split("&")) { | ||
| int split = pair.indexOf('='); | ||
| String rawKey = split >= 0 ? pair.substring(0, split) : pair; | ||
| if (!key.equals(decode(rawKey))) { | ||
| continue; | ||
| } | ||
| return decode(split >= 0 ? pair.substring(split + 1) : ""); | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| private static String decode(String value) { | ||
| try { | ||
| return URLDecoder.decode(value, StandardCharsets.UTF_8.name()); | ||
| } catch (Exception ignored) { | ||
| return ""; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package de.starlightunit.wrapper.navigation; | ||
|
|
||
| import android.content.ActivityNotFoundException; | ||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.net.Uri; | ||
|
|
||
| public final class ExternalLinkLauncher { | ||
| private ExternalLinkLauncher() { | ||
| } | ||
|
|
||
| public static boolean open(Context context, String url) { | ||
| if (!LinkRoutingPolicy.isAllowedExternalScheme(url)) { | ||
| return false; | ||
| } | ||
| try { | ||
| Intent intent; | ||
| String scheme = Uri.parse(url).getScheme(); | ||
| if ("intent".equalsIgnoreCase(scheme)) { | ||
| intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); | ||
| intent.setComponent(null); | ||
| intent.setSelector(null); | ||
| } else { | ||
| intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); | ||
| } | ||
| intent.addCategory(Intent.CATEGORY_BROWSABLE); | ||
| context.startActivity(intent); | ||
| return true; | ||
| } catch (ActivityNotFoundException | SecurityException | java.net.URISyntaxException ignored) { | ||
| return false; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a generated profile sets
DEEP_LINK_SCHEME, Android still cannot deliver matching custom-scheme URLs to this activity: the repository-wide manifest contains only theMAIN/LAUNCHERintent filter and noVIEWfilter with the configured scheme. Consequently, launching a URI such asquantum:///news/42finds no matching activity, so the newresolveDeepLinkTarget()andonNewIntent()paths are never reached. The profile/build output must also add a browsable custom-scheme intent filter.Useful? React with 👍 / 👎.