Skip to content

Commit

Permalink
implement deeplink gradle plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielittner committed Oct 6, 2023
1 parent cdcc3a0 commit 87026ac
Show file tree
Hide file tree
Showing 12 changed files with 657 additions and 71 deletions.
6 changes: 5 additions & 1 deletion deeplinks-plugin/deeplinks-plugin.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
plugins {
alias(libs.plugins.fgp.gradle)
// alias(libs.plugins.fgp.publish)
alias(libs.plugins.fgp.publish)
alias(libs.plugins.kotlin.serialization)
}

dependencies {
compileOnly(libs.android.gradle.api)
implementation(libs.toml)
implementation(libs.serialization)
implementation(projects.navigation)

testImplementation(libs.junit)
testImplementation(libs.truth)
Expand Down
8 changes: 5 additions & 3 deletions deeplinks-plugin/rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
-keepattributes Signature,Exceptions,*Annotation*,InnerClasses,PermittedSubclasses,EnclosingMethod,Deprecated,SourceFile,LineNumberTable

# Keep your public API so that it's callable from scripts
-keep class com.freeletics.khonshu.deeplinks.plugin.*Extension { *; }
-keep class com.freeletics.khonshu.deeplinks.plugin.*Plugin { *; }
-keep class com.freeletics.khonshu.deeplinks.plugin.*Task { *; }
-keep class com.freeletics.khonshu.deeplinks.plugin.** { public *; }
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
synthetic <init>(...);
}

# No need to obfuscate class names
-dontobfuscate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.freeletics.khonshu.deeplinks.plugin

import com.freeletics.khonshu.navigation.deeplinks.DeepLinkHandler
import com.freeletics.khonshu.navigation.deeplinks.DeepLinkHandler.Pattern
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable
public data class Configuration(
val prefixes: List<Prefix> = emptyList(),
val placeholders: List<Placeholder> = emptyList(),
val deepLinks: Map<String, DeepLink>,
)

@Serializable
public data class DeepLink(
val patterns: List<
@Serializable(PatternSerializer::class)
Pattern,
>,
val prefixes: List<Prefix>? = null, // use global if null
val placeholders: List<Placeholder>? = null, // use global if null or if key not found
)

@Serializable
public data class Prefix(
val scheme: String,
val host: String,
val autoVerified: Boolean,
) {
init {
// for validation purposes
DeepLinkHandler.Prefix("$scheme://$host")
}
}

@Serializable
public data class Placeholder(
val key: String,
val values: List<String>,
)

internal object PatternSerializer : KSerializer<Pattern> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Pattern", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): Pattern {
return Pattern(decoder.decodeString())
}

override fun serialize(encoder: Encoder, value: Pattern) {
throw UnsupportedOperationException()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.freeletics.khonshu.deeplinks.plugin

import com.freeletics.khonshu.navigation.deeplinks.DeepLinkHandler
import com.freeletics.khonshu.navigation.deeplinks.replacePlaceholders
import com.freeletics.khonshu.navigation.internal.InternalNavigationApi
import java.io.File
import net.peanuuutz.tomlkt.Toml

private const val PLACEHOLDER = "<!-- DEEPLINK INTENT FILTERS -->"

internal fun configure(
configurationFile: File,
inputManifestFile: File,
outputManifestFile: File,
) {
val manifest = inputManifestFile.readLines().toMutableList()
val placeholderIndex = manifest.indexOfFirst { it.contains(PLACEHOLDER) }
check(placeholderIndex >= 0) {
"Did not find $PLACEHOLDER in given manifest ${inputManifestFile.absolutePath}"
}

val configuration = Toml.decodeFromString(Configuration.serializer(), configurationFile.readText())
val indentation = manifest[placeholderIndex].takeWhile { it == ' ' }
manifest[placeholderIndex] = intentFiltersFromConfig(configuration, indentation)

outputManifestFile.writeText(manifest.joinToString(separator = "\n"))
}

private fun intentFiltersFromConfig(configuration: Configuration, indentation: String): String {
val builder = IntentFilterBuilder(indentation)

val deepLinksWithGlobalPrefixes = configuration.deepLinks.values.filter { it.prefixes == null }
if (deepLinksWithGlobalPrefixes.isNotEmpty()) {
check(configuration.prefixes.isNotEmpty()) {
"Configuration contains deep links without a prefix but has no global prefixes"
}

configuration.prefixes.forEach { prefix ->
builder.appendIntentFilter(prefix, deepLinksWithGlobalPrefixes)
}
}

configuration.deepLinks.values.filter { it.prefixes != null }.forEach {
it.prefixes!!.forEach { prefix ->
builder.appendIntentFilter(prefix, listOf(it))
}
}

return builder.toString().trim()
}

private fun IntentFilterBuilder.appendIntentFilter(prefix: Prefix, deepLinks: List<DeepLink>) {
start(prefix.autoVerified)
appendAction("android.intent.action.VIEW")
appendCategory("android.intent.category.DEFAULT")
appendCategory("android.intent.category.BROWSABLE")

deepLinks.forEach { deepLink ->
deepLink.patterns.forEach { pattern ->
appendData(prefix.scheme, prefix.host, pattern)
}
}
end()
}

private class IntentFilterBuilder(
private val indentation: String,
) {
private val builder = StringBuilder()

fun start(autoVerify: Boolean) {
builder.appendLine("$indentation<intent-filter android:autoVerify=\"${autoVerify}\">")
}

fun appendAction(action: String) {
builder.appendLine("$indentation <action android:name=\"${action}\" />")
}

fun appendCategory(category: String) {
builder.appendLine("$indentation <action android:name=\"${category}\" />")
}

@OptIn(InternalNavigationApi::class)
fun appendData(scheme: String, host: String, pathPattern: DeepLinkHandler.Pattern) {
builder.appendLine("$indentation <data")
builder.appendLine("$indentation android:scheme=\"${scheme}\"")
builder.appendLine("$indentation android:host=\"${host}\"")
builder.appendLine("$indentation android:pathPattern=\"/${pathPattern.replacePlaceholders(".*")}\"")
builder.appendLine("$indentation />")
}

fun end() {
builder.appendLine("$indentation</intent-filter>")
}

override fun toString(): String {
return builder.toString()
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,28 @@
package com.freeletics.khonshu.deeplinks.plugin

import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class DeeplinksManifestConfiguratorTask : DefaultTask() {
public abstract class DeeplinksManifestConfiguratorTask : DefaultTask() {

@get:InputFile
abstract val deeplinksConfigurationFile: RegularFileProperty
public abstract val deeplinksConfigurationFile: RegularFileProperty

@get:InputFile
abstract val mergedManifest: RegularFileProperty
public abstract val mergedManifest: RegularFileProperty

@get:OutputFile
abstract val updatedManifest: RegularFileProperty
public abstract val updatedManifest: RegularFileProperty

@TaskAction
fun taskAction() {
public fun taskAction() {
val configurationFile = deeplinksConfigurationFile.get().asFile
val inputManifest = mergedManifest.get().asFile
val outputManifest = updatedManifest.get().asFile

DeeplinksManifestConfigurator(configurationFile, inputManifest, outputManifest).configure()
}
}

internal class DeeplinksManifestConfigurator(
private val configurationFile: File,
private val inputManifestFile: File,
private val outputManifestFile: File,
) {
fun configure() {
val configuration = configurationFile.readText()

var manifest = inputManifestFile.readText()
manifest = manifest.replace(PLACEHOLDER, configuration)

outputManifestFile.writeText(manifest)
}

companion object {
const val PLACEHOLDER = "<!-- DEEPLINK INTENT FILTERS -->"
configure(configurationFile, inputManifest, outputManifest)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.gradle.api.Project
* Plugin for deeplinks integration:
* - appends intent filters to app's manifest for deeplinks from configuration file
*/
abstract class DeeplinksPlugin : Plugin<Project> {
public abstract class DeeplinksPlugin : Plugin<Project> {
override fun apply(project: Project) {
val configurationFile = File(project.projectDir, "deeplinks.toml")
if (!configurationFile.exists()) {
Expand Down
Loading

0 comments on commit 87026ac

Please sign in to comment.