diff --git a/README.md b/README.md index 2dce248..e93b418 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The project is organized in the following modules: - `tool-googleplay` - contains a class wrapper for Google Play - `tool-timber` - contains a class wrapper for Timber - `tool-networkemulator-okhttp` - contains a class wrapper for NetworkEmulator +- `tool-showkase` - contains a class wrapper for Showkase +- `tool-showkase-no-op` - contains stubs for easy release implementation of the Showkase wrapper - `sample` - a sample app for testing and developing @@ -105,6 +107,8 @@ debugImplementation "com.infinum.sentinel:tool-appgallery:$sentinelVersion" debugImplementation "com.infinum.sentinel:tool-googleplay:$sentinelVersion" debugImplementation "com.infinum.sentinel:tool-timber:$sentinelVersion" debugImplementation "com.infinum.sentinel:tool-networkemulator-okhttp:$sentinelVersion" +debugImplementation "com.infinum.sentinel:tool-showkase:$sentinelVersion" +releaseImplementation "com.infinum.sentinel:tool-showkase-no-op:$sentinelVersion" ``` **KotlinDSL** @@ -118,6 +122,8 @@ debugImplementation("com.infinum.sentinel:tool-appgallery:$sentinelVersion") debugImplementation("com.infinum.sentinel:tool-googleplay:$sentinelVersion") debugImplementation("com.infinum.sentinel:tool-timber:$sentinelVersion") debugImplementation("com.infinum.sentinel:tool-networkemulator-okhttp:$sentinelVersion") +debugImplementation("com.infinum.sentinel:tool-showkase:$sentinelVersion") +releaseImplementation("com.infinum.sentinel:tool-showkase-no-op:$sentinelVersion") ``` Now you can sync your project. @@ -202,6 +208,36 @@ Depending of what you include as module dependencies, very specific tools are pr - `GooglePlayTool` - a wrapper class that opens Google Play of a published application or a web page of the application if Google Play is not found - `NetworkEmulatorTool` - a wrapper class that opens [NetworkEmulator](https://github.com/infinum/android-sentinel/tree/master/tool-networkemulator-okhttp). Enables user to modify network conditions. +- `ShowkaseTool` - a wrapper class that opens [Showkase](https://github.com/airbnb/Showkase), Airbnb's + Compose component browser. Requires additional setup, see [Showkase](#showkase) below. + +##### Showkase + +Unlike the other wrappers, Showkase needs build configuration in your project, because its +annotation processor has to run over your own sources. The short version: + +```groovy +debugImplementation "com.infinum.sentinel:tool-showkase:$sentinelVersion" +releaseImplementation "com.infinum.sentinel:tool-showkase-no-op:$sentinelVersion" +debugImplementation "com.airbnb.android:showkase:1.0.5" +kspDebug "com.airbnb.android:showkase-processor:1.0.5" +``` + +Use a variant-scoped `ksp` configuration such as `kspDebug`, never the project-wide `ksp` +configuration: that one applies the processor to every variant, production included. + +Register the tool with your app's `@ShowkaseRoot` module: + +```kotlin +Sentinel.watch( + setOf( + ShowkaseTool(MyRootModule::class), + ), +) +``` + +[tool-showkase/README.md](tool-showkase/README.md) covers the full setup, flavored projects, +minified builds and troubleshooting. #### Source abstractions diff --git a/build.gradle.kts b/build.gradle.kts index 4cc25e2..9b252a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,7 +73,9 @@ tasks.register("lintAll") { ":tool-dbinspector:lintRelease", ":tool-leakcanary:lintRelease", ":tool-appgallery:lintRelease", - ":tool-googleplay:lintRelease" + ":tool-googleplay:lintRelease", + ":tool-showkase:lintRelease", + ":tool-showkase-no-op:lintRelease" ) group = "Verification" description = "Run Detekt on all modules" @@ -88,7 +90,9 @@ tasks.register("detektAll") { ":tool-dbinspector:detekt", ":tool-leakcanary:detekt", ":tool-appgallery:detekt", - ":tool-googleplay:detekt" + ":tool-googleplay:detekt", + ":tool-showkase:detekt", + ":tool-showkase-no-op:detekt" ) group = "Verification" description = "Run Detekt on all modules" diff --git a/deploy.gradle.kts b/deploy.gradle.kts index 2e74e0f..75310e8 100644 --- a/deploy.gradle.kts +++ b/deploy.gradle.kts @@ -90,6 +90,17 @@ tasks.register("deployToolNetworkEmulatorOkhttp") { description = "Deploy Tool NetworkEmulator OkHttp modules to Maven Central repository" } +tasks.register("deployToolShowkase") { + dependsOn( + ":tool-showkase:clean", + ":tool-showkase-no-op:clean", + ":tool-showkase:publishMavenPublicationToMavenCentralRepository", + ":tool-showkase-no-op:publishMavenPublicationToMavenCentralRepository" + ) + group = "Deploy" + description = "Deploy Tool Showkase modules to Maven Central repository" +} + tasks.register("deploySentinelAll") { dependsOn( "deploySentinel", @@ -108,7 +119,8 @@ tasks.register("deployToolsAll") { "deployToolAppGallery", "deployToolGooglePlay", "deployToolTimber", - "deployToolNetworkEmulatorOkhttp" + "deployToolNetworkEmulatorOkhttp", + "deployToolShowkase" ) group = "Deploy" description = "Deploy all tools modules to repositories" @@ -125,7 +137,8 @@ tasks.register("deployAll") { "deployToolAppGallery", "deployToolGooglePlay", "deployToolTimber", - "deployToolNetworkEmulatorOkhttp" + "deployToolNetworkEmulatorOkhttp", + "deployToolShowkase" ) group = "Deploy" description = "Deploy all modules to Maven Central repository" @@ -144,6 +157,8 @@ tasks.register("deployDebug") { ":tool-timber:clean", ":tool-networkemulator-okhttp:clean", ":tool-networkemulator-okhttp-no-op:clean", + ":tool-showkase:clean", + ":tool-showkase-no-op:clean", ":sentinel:publishMavenPublicationToMavenLocal", ":sentinel-no-op:publishMavenPublicationToMavenLocal", ":tool-chucker:publishMavenPublicationToMavenLocal", @@ -154,7 +169,9 @@ tasks.register("deployDebug") { ":tool-googleplay:publishMavenPublicationToMavenLocal", ":tool-timber:publishMavenPublicationToMavenLocal", ":tool-networkemulator-okhttp:publishMavenPublicationToMavenLocal", - ":tool-networkemulator-okhttp-no-op:publishMavenPublicationToMavenLocal" + ":tool-networkemulator-okhttp-no-op:publishMavenPublicationToMavenLocal", + ":tool-showkase:publishMavenPublicationToMavenLocal", + ":tool-showkase-no-op:publishMavenPublicationToMavenLocal" ) group = "Deploy" description = "Deploy all modules to Maven Local repository for debugging" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0662231..426087e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ appcompat = "1.8.0" binaryCompatibility = "0.18.1" chucker = "4.3.1" collar = "2.1.0" +compose = "1.12.0" core = "1.19.0" coroutines = "1.11.0" crypto = "1.1.0" @@ -37,6 +38,7 @@ robolectricShadow = "4.16.1" room = "2.8.4" roomTest = "2.8.4" sentinel = "2.0.0" +showkase = "1.0.5" startup = "1.2.0" timber = "5.0.1" workmanager = "2.11.2" @@ -53,6 +55,8 @@ toolgoogleplay = { module = "com.infinum.sentinel:tool-googleplay", version.ref tooltimber = { module = "com.infinum.sentinel:tool-timber", version.ref = "sentinel" } toolnetworkemulatorokhttp = { module = "com.infinum.sentinel:tool-networkemulator-okhttp", version.ref = "sentinel" } toolnetworkemulatorokhttpnoop = { module = "com.infinum.sentinel:tool-networkemulator-okhttp-no-op", version.ref = "sentinel" } +toolshowkase = { module = "com.infinum.sentinel:tool-showkase", version.ref = "sentinel" } +toolshowkasenoop = { module = "com.infinum.sentinel:tool-showkase-no-op", version.ref = "sentinel" } tools-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } @@ -117,6 +121,13 @@ robolectric-shadowapi = { module = "org.robolectric:shadowapi", version.ref = "r okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } +showkase-annotation = { module = "com.airbnb.android:showkase-annotation", version.ref = "showkase" } +showkase-processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } + +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } +compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } + [bundles] androidx = [ "androidx-core", @@ -150,4 +161,5 @@ tools = [ [plugins] gradle-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "gradle-maven-publish" } kotlin-binary-compatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibility" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 83ac973..3bc0ea7 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -7,6 +7,8 @@ val releaseConfig = extra["releaseConfig"] as Map plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) } android { @@ -93,4 +95,19 @@ dependencies { debugImplementation(project(":tool-networkemulator-okhttp")) releaseImplementation(project(":tool-networkemulator-okhttp-no-op")) debugImplementation(project(":tool-timber")) + + debugImplementation(project(":tool-showkase")) + releaseImplementation(project(":tool-showkase-no-op")) + + debugImplementation(libs.showkase) + debugImplementation(libs.compose.runtime) + debugImplementation(libs.compose.material) + + // The Compose compiler plugin applies to every variant and refuses to run without the + // runtime on the compile classpath, even though release has no composables + releaseCompileOnly(libs.compose.runtime) + + // Variant-scoped on purpose. The project-wide `ksp` configuration would run the + // processor for release, where the browser runtime does not exist. + kspDebug(libs.showkase.processor) } diff --git a/sample/src/debug/kotlin/com/infinum/sentinel/sample/ShowkaseSamples.kt b/sample/src/debug/kotlin/com/infinum/sentinel/sample/ShowkaseSamples.kt new file mode 100644 index 0000000..e2d2d8d --- /dev/null +++ b/sample/src/debug/kotlin/com/infinum/sentinel/sample/ShowkaseSamples.kt @@ -0,0 +1,21 @@ +package com.infinum.sentinel.sample + +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import com.airbnb.android.showkase.annotation.ShowkaseComposable + +@ShowkaseComposable(name = "Primary button", group = "Buttons") +@Composable +fun PrimaryButtonSample() { + Button(onClick = {}) { + Text(text = "Show Sentinel") + } +} + +@ShowkaseComposable(name = "Title", group = "Text") +@Composable +fun TitleTextSample() { + Text(text = "Sentinel sample", style = MaterialTheme.typography.h6) +} diff --git a/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleApplication.kt b/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleApplication.kt index 9d47380..2d91489 100644 --- a/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleApplication.kt +++ b/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleApplication.kt @@ -11,6 +11,7 @@ import com.infinum.sentinel.ui.tools.DbInspectorTool import com.infinum.sentinel.ui.tools.GooglePlayTool import com.infinum.sentinel.ui.tools.LeakCanaryTool import com.infinum.sentinel.ui.tools.NetworkEmulatorTool +import com.infinum.sentinel.ui.tools.ShowkaseTool import com.infinum.sentinel.ui.tools.TimberTool import java.security.cert.CertificateFactory import java.security.cert.X509Certificate @@ -35,6 +36,7 @@ class SampleApplication : Application() { tools.add(AppGalleryTool(appId = "102016595")) tools.add(GooglePlayTool()) tools.add(TimberTool(allowedTags = listOf("Main"))) + tools.add(ShowkaseTool(SampleShowkaseRootModule::class)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { tools.add(CertificateTool(userCertificates = loadDebugCertificates())) } diff --git a/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleShowkaseRootModule.kt b/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleShowkaseRootModule.kt new file mode 100644 index 0000000..0720ef3 --- /dev/null +++ b/sample/src/main/kotlin/com/infinum/sentinel/sample/SampleShowkaseRootModule.kt @@ -0,0 +1,7 @@ +package com.infinum.sentinel.sample + +import com.airbnb.android.showkase.annotation.ShowkaseRoot +import com.airbnb.android.showkase.annotation.ShowkaseRootModule + +@ShowkaseRoot +class SampleShowkaseRootModule : ShowkaseRootModule diff --git a/settings.gradle.kts b/settings.gradle.kts index 59ed825..1e0f6eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,8 @@ include(":tool-googleplay") include(":tool-appgallery") include(":tool-networkemulator-okhttp") include(":tool-networkemulator-okhttp-no-op") +include(":tool-showkase") +include(":tool-showkase-no-op") include(":tool-timber") gradle.startParameter.excludedTaskNames.add(":buildSrc:testClasses") diff --git a/tool-showkase-no-op/.gitignore b/tool-showkase-no-op/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/tool-showkase-no-op/.gitignore @@ -0,0 +1 @@ +/build diff --git a/tool-showkase-no-op/api/tool-showkase-no-op.api b/tool-showkase-no-op/api/tool-showkase-no-op.api new file mode 100644 index 0000000..24fb8b9 --- /dev/null +++ b/tool-showkase-no-op/api/tool-showkase-no-op.api @@ -0,0 +1,12 @@ +public final class com/infinum/sentinel/ui/tools/ShowkaseTool : com/infinum/sentinel/Sentinel$Tool { + public fun (Lkotlin/reflect/KClass;)V + public final fun copy (Lkotlin/reflect/KClass;)Lcom/infinum/sentinel/ui/tools/ShowkaseTool; + public static synthetic fun copy$default (Lcom/infinum/sentinel/ui/tools/ShowkaseTool;Lkotlin/reflect/KClass;ILjava/lang/Object;)Lcom/infinum/sentinel/ui/tools/ShowkaseTool; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun icon ()Ljava/lang/Integer; + public fun listener ()Landroid/view/View$OnClickListener; + public fun name ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/tool-showkase-no-op/build.gradle.kts b/tool-showkase-no-op/build.gradle.kts new file mode 100644 index 0000000..10f6b90 --- /dev/null +++ b/tool-showkase-no-op/build.gradle.kts @@ -0,0 +1,79 @@ +@Suppress("UNCHECKED_CAST") +val buildConfig = extra["buildConfig"] as Map +@Suppress("UNCHECKED_CAST") +val releaseConfig = extra["releaseConfig"] as Map + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + alias(libs.plugins.gradle.maven.publish) +} + +android { + buildFeatures { + buildConfig = false + } + + compileSdk = buildConfig["compileSdk"] as Int + buildToolsVersion = buildConfig["buildTools"] as String + + defaultConfig { + minSdk = buildConfig["minSdk"] as Int + } + + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.txt") + } + } + + namespace = "com.infinum.sentinel.tool.showkase" + resourcePrefix = "sentinel_" + + kotlin { + jvmToolchain(11) + + compilerOptions { + freeCompilerArgs.addAll( + listOf( + "-Xexplicit-api=strict", + "-Xjvm-default=all" + ) + ) + } + } + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + } + +} + +tasks.named("dokkaGenerate") { + mustRunAfter(":tool-showkase-no-op:generateReleaseRFile") +} + +dependencies { + implementation(libs.kotlin.core) + // Sentinel is provided by the app. Production variants use sentinel-no-op, and declaring the + // real library as api here would drag it into those builds. + compileOnly(libs.library) + // Kotlin-only artifact, no Compose. Keeps the app's @ShowkaseRoot class resolvable in production. + api(libs.showkase.annotation) +} + +val groupId: String by project + +mavenPublishing { + coordinates( + groupId = groupId, + artifactId = "tool-showkase-no-op", + version = releaseConfig["version"] as String + ) +} diff --git a/tool-showkase-no-op/proguard-rules.txt b/tool-showkase-no-op/proguard-rules.txt new file mode 100644 index 0000000..7317b42 --- /dev/null +++ b/tool-showkase-no-op/proguard-rules.txt @@ -0,0 +1,4 @@ +# Showkase No-op - ProGuard Rules + +# Keep the public API class - no-op implementations should be preserved +-keep public class com.infinum.sentinel.ui.tools.ShowkaseTool { *; } diff --git a/tool-showkase-no-op/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt b/tool-showkase-no-op/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt new file mode 100644 index 0000000..f7c13f6 --- /dev/null +++ b/tool-showkase-no-op/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt @@ -0,0 +1,27 @@ +package com.infinum.sentinel.ui.tools + +import android.view.View +import com.airbnb.android.showkase.annotation.ShowkaseRootModule +import com.infinum.sentinel.Sentinel +import kotlin.reflect.KClass + +/** + * No-op implementation of the Showkase wrapper tool. + * + * Maintains API compatibility with the debug implementation so that registering + * `ShowkaseTool(MyRootModule::class)` from shared sources compiles in every variant. + * + * @param rootModule the app's own implementation of ShowkaseRootModule, ignored here + */ +public data class ShowkaseTool( + @Suppress("unused") private val rootModule: KClass, +) : Sentinel.Tool { + + override fun icon(): Int? = null + + override fun name(): Int = 0 + + override fun listener(): View.OnClickListener = View.OnClickListener { + // No-op + } +} diff --git a/tool-showkase/.gitignore b/tool-showkase/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/tool-showkase/.gitignore @@ -0,0 +1 @@ +/build diff --git a/tool-showkase/README.md b/tool-showkase/README.md new file mode 100644 index 0000000..f982f07 --- /dev/null +++ b/tool-showkase/README.md @@ -0,0 +1,85 @@ +# Showkase Tool + +A wrapper that opens [Showkase](https://github.com/airbnb/Showkase), Airbnb's Compose component +browser, from Sentinel's Tools screen. Showkase renders your `@Preview` composables, colors and +typography as a browsable, on-device design-system catalogue. + +The tool is only a launcher. Showkase's annotation processor has to run over your own sources, so +the Showkase build setup stays in your app. + +## Setup + +**`app/build.gradle`** + +```groovy +dependencies { + debugImplementation "com.infinum.sentinel:tool-showkase:x.x.x" + releaseImplementation "com.infinum.sentinel:tool-showkase-no-op:x.x.x" + + // The browser runtime. It carries the whole Compose stack, so keep it out of release. + debugImplementation "com.airbnb.android:showkase:x.x.x" + + // The processor. Use a variant-scoped configuration, never the project-wide `ksp`, + // and keep it on the same version as the runtime above. + kspDebug "com.airbnb.android:showkase-processor:x.x.x" +} +``` + +Declare your root module once, anywhere in `main` sources: + +```kotlin +import com.airbnb.android.showkase.annotation.ShowkaseRoot +import com.airbnb.android.showkase.annotation.ShowkaseRootModule + +@ShowkaseRoot +class MyRootModule : ShowkaseRootModule +``` + +Register the tool: + +```kotlin +Sentinel.watch( + setOf( + ShowkaseTool(MyRootModule::class), + ), +) +``` + +Your root module, previews and registration all stay in `main` and are shared by every variant. +The Showkase annotations resolve everywhere, release included, because both tool modules expose +`showkase-annotation` transitively, a Kotlin-only artifact with no Compose in it. + +For flavored projects, scope the same dependencies per flavor instead of per build type, for +example `developmentImplementation` and `kspDevelopment`. KSP creates one `ksp` +configuration per source set, so flavor, build type and combined variants are all available. + +### ⚠️ Do not use the project-wide `ksp` configuration + +```groovy +// Wrong: applies the processor to every variant, production included. +ksp "com.airbnb.android:showkase-processor:x.x.x" +``` + +Code generated for production references the browser runtime, so either the Compose payload ships +to production too, or production stops compiling. Always name the variant, such as `kspDebug`. + +## Compose stays yours + +`tool-showkase` declares the browser runtime as `compileOnly`, so it never contributes a Compose +version to your dependency resolution. Your app supplies `com.airbnb.android:showkase` to the +variants that need it. If a variant gets the tool without the runtime, tapping the tool logs an +error to logcat under the `Sentinel` tag instead of crashing. + +## Minified builds + +No extra configuration is needed: `tool-showkase` ships consumer keep rules +([`consumer-rules.pro`](consumer-rules.pro)) for your `@ShowkaseRoot` class and the generated code +Showkase resolves by name. + +## Troubleshooting + +**Tapping the tool does nothing.** Check logcat under the `Sentinel` tag. The variant has +`tool-showkase` but is missing `com.airbnb.android:showkase`. + +**The browser opens but the catalogue is empty.** The processor did not run for this variant, +no class is annotated with `@ShowkaseRoot`, or nothing is annotated for Showkase to pick up. diff --git a/tool-showkase/api/tool-showkase.api b/tool-showkase/api/tool-showkase.api new file mode 100644 index 0000000..24fb8b9 --- /dev/null +++ b/tool-showkase/api/tool-showkase.api @@ -0,0 +1,12 @@ +public final class com/infinum/sentinel/ui/tools/ShowkaseTool : com/infinum/sentinel/Sentinel$Tool { + public fun (Lkotlin/reflect/KClass;)V + public final fun copy (Lkotlin/reflect/KClass;)Lcom/infinum/sentinel/ui/tools/ShowkaseTool; + public static synthetic fun copy$default (Lcom/infinum/sentinel/ui/tools/ShowkaseTool;Lkotlin/reflect/KClass;ILjava/lang/Object;)Lcom/infinum/sentinel/ui/tools/ShowkaseTool; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun icon ()Ljava/lang/Integer; + public fun listener ()Landroid/view/View$OnClickListener; + public fun name ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/tool-showkase/build.gradle.kts b/tool-showkase/build.gradle.kts new file mode 100644 index 0000000..ab058f5 --- /dev/null +++ b/tool-showkase/build.gradle.kts @@ -0,0 +1,81 @@ +@Suppress("UNCHECKED_CAST") +val buildConfig = extra["buildConfig"] as Map +@Suppress("UNCHECKED_CAST") +val releaseConfig = extra["releaseConfig"] as Map + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + alias(libs.plugins.gradle.maven.publish) +} + +android { + buildFeatures { + buildConfig = false + } + + compileSdk = buildConfig["compileSdk"] as Int + buildToolsVersion = buildConfig["buildTools"] as String + + defaultConfig { + minSdk = buildConfig["minSdk"] as Int + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.txt") + } + } + + namespace = "com.infinum.sentinel.tool.showkase" + resourcePrefix = "sentinel_" + + kotlin { + jvmToolchain(11) + + compilerOptions { + freeCompilerArgs.addAll( + listOf( + "-Xexplicit-api=strict", + "-Xjvm-default=all" + ) + ) + } + } + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + } + +} + +tasks.named("dokkaGenerate") { + mustRunAfter(":tool-showkase:generateReleaseRFile") +} + +dependencies { + implementation(libs.kotlin.core) + api(libs.library) + // Kotlin-only artifact, no Compose. Lets the app's @ShowkaseRoot class resolve in every variant. + api(libs.showkase.annotation) + // The browser runtime carries the whole Compose stack, so it must never be contributed to the + // app's dependency resolution. The app supplies it per variant instead. + compileOnly(libs.showkase) +} + +val groupId: String by project + +mavenPublishing { + coordinates( + groupId = groupId, + artifactId = "tool-showkase", + version = releaseConfig["version"] as String + ) +} diff --git a/tool-showkase/consumer-rules.pro b/tool-showkase/consumer-rules.pro new file mode 100644 index 0000000..c3cd3a7 --- /dev/null +++ b/tool-showkase/consumer-rules.pro @@ -0,0 +1,9 @@ +# ShowkaseBrowserActivity appends "Codegen" to the canonical name of the app's @ShowkaseRoot module +# and calls Class.forName on the result, so both names have to survive obfuscation. Showkase's own +# AAR ships an empty proguard.txt, so the rules live here. +-keep class * implements com.airbnb.android.showkase.annotation.ShowkaseRootModule +-keep @com.airbnb.android.showkase.annotation.ShowkaseRootCodegen class * { *; } + +# The browser runtime is compileOnly, so ShowkaseTool's reference to it dangles in variants that +# did not supply it, and R8 fails those with "Missing class". +-dontwarn com.airbnb.android.showkase.ui.** diff --git a/tool-showkase/proguard-rules.txt b/tool-showkase/proguard-rules.txt new file mode 100644 index 0000000..e55fd36 --- /dev/null +++ b/tool-showkase/proguard-rules.txt @@ -0,0 +1,5 @@ +-keeppackagenames +-keep public class com.infinum.sentinel.ui.tools.ShowkaseTool { + public protected *; +} +-keep public class com.infinum.sentinel.databinding.** diff --git a/tool-showkase/src/main/AndroidManifest.xml b/tool-showkase/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cc947c5 --- /dev/null +++ b/tool-showkase/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/tool-showkase/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt b/tool-showkase/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt new file mode 100644 index 0000000..714cd45 --- /dev/null +++ b/tool-showkase/src/main/kotlin/com/infinum/sentinel/ui/tools/ShowkaseTool.kt @@ -0,0 +1,45 @@ +package com.infinum.sentinel.ui.tools + +import android.content.Intent +import android.util.Log +import android.view.View +import com.airbnb.android.showkase.annotation.ShowkaseRootModule +import com.airbnb.android.showkase.ui.ShowkaseBrowserActivity +import com.infinum.sentinel.Sentinel +import com.infinum.sentinel.tool.showkase.R +import kotlin.reflect.KClass + +/** + * Specific wrapper tool around Showkase. + * + * Opens the Showkase browser for the app's own root module, passed in as [rootModule]. + * Tool Activity will launch with FLAG_ACTIVITY_SINGLE_TOP and FLAG_ACTIVITY_NEW_TASK flags. + * + * The consuming app supplies the Showkase browser runtime and applies the Showkase KSP + * processor itself, variant-scoped, as described in this module's README. + * + * @param rootModule the app's own implementation of ShowkaseRootModule, annotated with `@ShowkaseRoot` + */ +public data class ShowkaseTool( + private val rootModule: KClass, +) : Sentinel.Tool { + + override fun icon(): Int? = null + + override fun name(): Int = R.string.sentinel_showkase + + override fun listener(): View.OnClickListener = View.OnClickListener { view -> + try { + view.context.startActivity( + ShowkaseBrowserActivity.getIntent(view.context, rootModule.java.canonicalName.orEmpty()).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + ) + } catch (error: NoClassDefFoundError) { + // The browser is compileOnly here, so this is what the runtime throws when the app + // did not supply it to this variant. + Log.e("Sentinel", "Add com.airbnb.android:showkase to the variants using tool-showkase.", error) + } + } +} diff --git a/tool-showkase/src/main/res/values/strings.xml b/tool-showkase/src/main/res/values/strings.xml new file mode 100644 index 0000000..e38bcc5 --- /dev/null +++ b/tool-showkase/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Showkase +