|
| 1 | +/* |
| 2 | + * Copyright 2026 The Android Open Source Project |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.example.jetnews.deeplink.util |
| 18 | + |
| 19 | +import android.net.Uri |
| 20 | +import androidx.navigation3.runtime.NavKey |
| 21 | +import java.io.Serializable |
| 22 | +import kotlinx.serialization.KSerializer |
| 23 | +import kotlinx.serialization.descriptors.PrimitiveKind |
| 24 | +import kotlinx.serialization.descriptors.SerialKind |
| 25 | +import kotlinx.serialization.encoding.CompositeDecoder |
| 26 | + |
| 27 | +/** |
| 28 | + * Parse a supported deeplink and stores its metadata as a easily readable format |
| 29 | + * |
| 30 | + * The following notes applies specifically to this particular sample implementation: |
| 31 | + * |
| 32 | + * The supported deeplink is expected to be built from a serializable backstack key [T] that |
| 33 | + * supports deeplink. This means that if this deeplink contains any arguments (path or query), |
| 34 | + * the argument name must match any of [T] member field name. |
| 35 | + * |
| 36 | + * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] |
| 37 | + * supports two deeplink patterns: |
| 38 | + * ``` |
| 39 | + * val deeplink1 = www.nav3recipes.com/home |
| 40 | + * val deeplink2 = www.nav3recipes.com/profile/{userId} |
| 41 | + * ``` |
| 42 | + * Then two [DeepLinkPattern] should be created |
| 43 | + * ``` |
| 44 | + * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) |
| 45 | + * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) |
| 46 | + * ``` |
| 47 | + * |
| 48 | + * This implementation assumes a few things: |
| 49 | + * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match |
| 50 | + * 2. all query arguments are optional by way of nullable/has default value |
| 51 | + * |
| 52 | + * @param T the backstack key type that supports the deeplinking of [uriPattern] |
| 53 | + * @param serializer the serializer of [T] |
| 54 | + * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" |
| 55 | + */ |
| 56 | +class DeepLinkPattern<T : NavKey>(val serializer: KSerializer<T>, val uriPattern: Uri) { |
| 57 | + /** |
| 58 | + * Help differentiate if a path segment is an argument or a static value |
| 59 | + */ |
| 60 | + private val regexPatternFillIn = Regex("\\{(.+?)\\}") |
| 61 | + |
| 62 | + // TODO make these lazy |
| 63 | + /** |
| 64 | + * parse the path into a list of [PathSegment] |
| 65 | + * |
| 66 | + * order matters here - path segments need to match in value and order when matching |
| 67 | + * requested deeplink to supported deeplink |
| 68 | + */ |
| 69 | + val pathSegments: List<PathSegment> = buildList { |
| 70 | + uriPattern.pathSegments.forEach { segment -> |
| 71 | + // first, check if it is a path arg |
| 72 | + var result = regexPatternFillIn.find(segment) |
| 73 | + if (result != null) { |
| 74 | + // if so, extract the path arg name (the string value within the curly braces) |
| 75 | + val argName = result.groups[1]!!.value |
| 76 | + // from [T], read the primitive type of this argument to get the correct type parser |
| 77 | + val elementIndex = serializer.descriptor.getElementIndex(argName) |
| 78 | + if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { |
| 79 | + throw IllegalArgumentException( |
| 80 | + "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'.", |
| 81 | + ) |
| 82 | + } |
| 83 | + |
| 84 | + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) |
| 85 | + // finally, add the arg name and its respective type parser to the map |
| 86 | + add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) |
| 87 | + } else { |
| 88 | + // if its not a path arg, then its just a static string path segment |
| 89 | + add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Parse supported queries into a map of queryParameterNames to [TypeParser] |
| 96 | + * |
| 97 | + * This will be used later on to parse a provided query value into the correct KType |
| 98 | + */ |
| 99 | + val queryValueParsers: Map<String, TypeParser> = buildMap { |
| 100 | + uriPattern.queryParameterNames.forEach { paramName -> |
| 101 | + val elementIndex = serializer.descriptor.getElementIndex(paramName) |
| 102 | + // Ignore static query parameters that are not in the Serializable class |
| 103 | + if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { |
| 104 | + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) |
| 105 | + this[paramName] = getTypeParser(elementDescriptor.kind) |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Metadata about a supported path segment |
| 112 | + */ |
| 113 | + class PathSegment(val stringValue: String, val isParamArg: Boolean, val typeParser: TypeParser) |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Parses a String into a Serializable Primitive |
| 118 | + */ |
| 119 | +private typealias TypeParser = (String) -> Serializable |
| 120 | + |
| 121 | +private fun getTypeParser(kind: SerialKind): TypeParser { |
| 122 | + return when (kind) { |
| 123 | + PrimitiveKind.STRING -> Any::toString |
| 124 | + |
| 125 | + PrimitiveKind.INT -> String::toInt |
| 126 | + |
| 127 | + PrimitiveKind.BOOLEAN -> String::toBoolean |
| 128 | + |
| 129 | + PrimitiveKind.BYTE -> String::toByte |
| 130 | + |
| 131 | + PrimitiveKind.CHAR -> String::toCharArray |
| 132 | + |
| 133 | + PrimitiveKind.DOUBLE -> String::toDouble |
| 134 | + |
| 135 | + PrimitiveKind.FLOAT -> String::toFloat |
| 136 | + |
| 137 | + PrimitiveKind.LONG -> String::toLong |
| 138 | + |
| 139 | + PrimitiveKind.SHORT -> String::toShort |
| 140 | + |
| 141 | + else -> throw IllegalArgumentException( |
| 142 | + "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive.", |
| 143 | + ) |
| 144 | + } |
| 145 | +} |
0 commit comments