diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4280f24..c22686a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } compose-gradle-plugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" } +compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet" } android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/modules/gradle-plugin/build.gradle.kts b/modules/gradle-plugin/build.gradle.kts index 089ae54..bc64d32 100644 --- a/modules/gradle-plugin/build.gradle.kts +++ b/modules/gradle-plugin/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(libs.compose.gradle.plugin) implementation(libs.kotlin.poet) testImplementation(libs.junit) + testImplementation(libs.compose.compiler.gradle.plugin) } group = "org.jetbrains.compose.storytale" diff --git a/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidComponentsHelper.kt b/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidComponentsHelper.kt new file mode 100644 index 0000000..c522961 --- /dev/null +++ b/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidComponentsHelper.kt @@ -0,0 +1,295 @@ +package org.jetbrains.compose.storytale.plugin + +import java.io.File +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.provider.Provider + +internal object AndroidComponentsHelper { + + fun configureApplicationMinSdk( + project: Project, + buildTypeName: String = StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX, + minSdkFloor: Int = 30, + ) { + val appComponents = project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + if (appComponents != null) { + val selector = appComponents.selector().withBuildType(buildTypeName) + appComponents.beforeVariants(selector) { variantBuilder -> + variantBuilder.minSdk = maxOf(variantBuilder.minSdk ?: 0, minSdkFloor) + } + } + } + + fun onVariant( + project: Project, + variantName: String, + onResolved: (variantName: String, applicationId: Provider) -> Unit, + ) { + // 1. Try typed ApplicationAndroidComponentsExtension (AGP 8.x / 9.x) + val appComponents = project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + if (appComponents != null) { + val selector = appComponents.selector().withBuildType(variantName) + appComponents.onVariants(selector) { variant -> + onResolved(variant.name, variant.applicationId) + } + return + } + + // 2. Fallback to reflective AndroidComponentsExtension + val components = project.extensions.findByName("androidComponents") + if (components != null) { + try { + val onVariantsMethod = components.javaClass.methods.firstOrNull { + it.name == "onVariants" && it.parameterCount == 2 && it.parameterTypes[1] == Action::class.java + } ?: components.javaClass.methods.firstOrNull { + it.name == "onVariants" && it.parameterCount == 1 && it.parameterTypes[0] == Action::class.java + } + + if (onVariantsMethod != null) { + val action = object : Action { + override fun execute(variant: Any) { + try { + val getNameMethod = variant.javaClass.methods.firstOrNull { it.name == "getName" && it.parameterCount == 0 } + val name = getNameMethod?.invoke(variant) as? String + if (name.equals(variantName, ignoreCase = true) || name?.contains(variantName, ignoreCase = true) == true) { + val getAppIdMethod = variant.javaClass.methods.firstOrNull { it.name == "getApplicationId" && it.parameterCount == 0 } + + @Suppress("UNCHECKED_CAST") + val appIdProvider = (getAppIdMethod?.invoke(variant) as? Provider) + ?: project.provider { resolveFallbackApplicationId(project, variantName) } + onResolved(name ?: variantName, appIdProvider) + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Error in onVariants callback", e) + } + } + } + + if (onVariantsMethod.parameterCount == 2) { + val selectorMethod = components.javaClass.getMethod("selector") + val selector = selectorMethod.invoke(components) + val withBuildTypeMethod = selector.javaClass.methods.firstOrNull { it.name == "withBuildType" && it.parameterCount == 1 } + val targetedSelector = withBuildTypeMethod?.invoke(selector, variantName) + ?: withBuildTypeMethod?.invoke(selector, variantName.lowercase()) + ?: selector.javaClass.getMethod("all").invoke(selector) + onVariantsMethod.invoke(components, targetedSelector, action) + return + } else { + onVariantsMethod.invoke(components, action) + return + } + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Failed to register onVariants via modern API", e) + } + } + + // 2. Legacy fallback for AGP 8.x / 7.x + val android = project.extensions.findByName("android") + if (android != null) { + try { + val getApplicationVariants = android.javaClass.methods.firstOrNull { it.name == "getApplicationVariants" } + if (getApplicationVariants != null) { + val variants = getApplicationVariants.invoke(android) + val configureEachMethod = variants?.javaClass?.methods?.firstOrNull { it.name == "configureEach" && it.parameterCount == 1 } + val action = object : Action { + override fun execute(variant: Any) { + try { + val name = variant.javaClass.getMethod("getName").invoke(variant) as? String + if (name.equals(variantName, ignoreCase = true) || name?.contains(variantName, ignoreCase = true) == true) { + val appId = (variant.javaClass.getMethod("getApplicationId").invoke(variant) as? String) + ?: resolveFallbackApplicationId(project, variantName) + onResolved(name ?: variantName, project.provider { appId }) + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Error in legacy variant callback", e) + } + } + } + if (configureEachMethod != null) { + configureEachMethod.invoke(variants, action) + return + } + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Legacy applicationVariants fallback failed", e) + } + } + + // 3. Fallback: project evaluation callback + project.afterEvaluate { + val appId = resolveFallbackApplicationId(project, variantName) + onResolved(variantName, project.provider { appId }) + } + } + + fun onDeviceTestVariant( + project: Project, + manifestFile: File? = null, + sourcesDir: File? = null, + onResolved: (variantName: String, applicationId: Provider) -> Unit, + ) { + val components = project.extensions.findByName("androidComponents") + if (components != null) { + try { + val onVariantsMethod = components.javaClass.methods.firstOrNull { + it.name == "onVariants" && it.parameterCount == 2 && it.parameterTypes[1] == Action::class.java + } ?: components.javaClass.methods.firstOrNull { + it.name == "onVariants" && it.parameterCount == 1 && it.parameterTypes[0] == Action::class.java + } + + if (onVariantsMethod != null) { + val action = object : Action { + override fun execute(variant: Any) { + try { + // 1. Check if variant has deviceTests (HasDeviceTests / HasAndroidTest) + val getDeviceTests = variant.javaClass.methods.firstOrNull { it.name == "getDeviceTests" } + val deviceTests = getDeviceTests?.invoke(variant) as? Map<*, *> + val deviceTest = deviceTests?.values?.firstOrNull() ?: run { + val getAndroidTest = variant.javaClass.methods.firstOrNull { it.name == "getAndroidTest" } + getAndroidTest?.invoke(variant) + } + + if (deviceTest != null) { + val getNameMethod = deviceTest.javaClass.methods.firstOrNull { it.name == "getName" && it.parameterCount == 0 } + val name = getNameMethod?.invoke(deviceTest) as? String ?: "androidDeviceTest" + val getAppIdMethod = deviceTest.javaClass.methods.firstOrNull { it.name == "getApplicationId" && it.parameterCount == 0 } + + @Suppress("UNCHECKED_CAST") + val appIdProvider = (getAppIdMethod?.invoke(deviceTest) as? Provider) + ?: project.provider { resolveFallbackApplicationId(project, "test") } + + if (manifestFile != null || sourcesDir != null) { + try { + val getSourcesMethod = deviceTest.javaClass.methods.firstOrNull { it.name == "getSources" } + val sources = getSourcesMethod?.invoke(deviceTest) + if (sources != null) { + if (manifestFile != null && manifestFile.exists()) { + val getManifestsMethod = sources.javaClass.methods.firstOrNull { it.name == "getManifests" } + val manifests = getManifestsMethod?.invoke(sources) + val addStaticManifestMethod = manifests?.javaClass?.methods?.firstOrNull { + it.name == "addStaticManifestFile" && it.parameterCount == 1 + } + addStaticManifestMethod?.invoke(manifests, manifestFile.absolutePath) + } + if (sourcesDir != null && sourcesDir.exists()) { + val getKotlinMethod = sources.javaClass.methods.firstOrNull { it.name == "getKotlin" } + val kotlinSources = getKotlinMethod?.invoke(sources) + val addStaticSourceMethod = kotlinSources?.javaClass?.methods?.firstOrNull { + it.name == "addStaticSourceDirectory" && it.parameterCount == 1 + } + addStaticSourceMethod?.invoke(kotlinSources, sourcesDir.absolutePath) + } + } + } catch (e: Throwable) { + project.logger.debug("Storytale: could not register sources via AGP Sources API", e) + } + } + + onResolved(name, appIdProvider) + return + } + + // 2. Check if variant itself is AndroidTest / DeviceTest + val getNameMethod = variant.javaClass.methods.firstOrNull { it.name == "getName" && it.parameterCount == 0 } + val variantName = getNameMethod?.invoke(variant) as? String + if (variantName?.contains("deviceTest", ignoreCase = true) == true || + variantName?.contains("androidTest", ignoreCase = true) == true + ) { + val getAppIdMethod = variant.javaClass.methods.firstOrNull { it.name == "getApplicationId" && it.parameterCount == 0 } + + @Suppress("UNCHECKED_CAST") + val appIdProvider = (getAppIdMethod?.invoke(variant) as? Provider) + ?: project.provider { resolveFallbackApplicationId(project, "test") } + onResolved(variantName, appIdProvider) + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Error in onDeviceTestVariant callback", e) + } + } + } + + val selectorMethod = components.javaClass.getMethod("selector") + val selector = selectorMethod.invoke(components) + val allMethod = selector.javaClass.getMethod("all") + val allSelector = allMethod.invoke(selector) + if (onVariantsMethod.parameterCount == 2) { + onVariantsMethod.invoke(components, allSelector, action) + } else { + onVariantsMethod.invoke(components, action) + } + return + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Failed to register onDeviceTestVariant", e) + } + } + + project.afterEvaluate { + val fallbackAppId = resolveFallbackApplicationId(project, "test") + onResolved("androidDeviceTest", project.provider { fallbackAppId }) + } + } + + internal fun resolveFallbackApplicationId(project: Project, variantName: String): String { + val android = project.extensions.findByName("android") + if (android != null) { + try { + val getDefaultConfig = android.javaClass.methods.firstOrNull { it.name == "getDefaultConfig" } + val defaultConfig = getDefaultConfig?.invoke(android) + val getApplicationId = defaultConfig?.javaClass?.methods?.firstOrNull { it.name == "getApplicationId" } + val baseAppId = getApplicationId?.invoke(defaultConfig) as? String + if (!baseAppId.isNullOrBlank()) { + return "$baseAppId.$variantName" + } + val getNamespace = android.javaClass.methods.firstOrNull { it.name == "getNamespace" } + val namespace = getNamespace?.invoke(android) as? String + if (!namespace.isNullOrBlank()) { + return "$namespace.$variantName" + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Could not inspect defaultConfig/namespace for fallback appId", e) + } + } + return "storytale.$variantName" + } + + fun resolveAdbPath(project: Project): String { + val androidHome = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") + if (!androidHome.isNullOrBlank()) { + val adb = File(androidHome, "platform-tools/adb") + if (adb.isFile && adb.canExecute()) { + return adb.absolutePath + } + } + val android = project.extensions.findByName("android") + if (android != null) { + try { + val getAdbExecutable = android.javaClass.methods.firstOrNull { it.name == "getAdbExecutable" } + val adbFile = getAdbExecutable?.invoke(android) as? File + if (adbFile != null && adbFile.isFile) { + return adbFile.absolutePath + } + } catch (e: Throwable) { + project.logger.debug("Storytale AndroidComponentsHelper: Could not get adb executable from android extension", e) + } + } + return "adb" + } + + fun resolveActiveDeviceSerial(adbPath: String): String? { + return try { + val output = runProcessIgnoreExit(adbPath, "devices") + val devices = output.trim().lines() + .drop(1) + .filter { it.isNotBlank() && it.contains("\tdevice") } + .map { it.split("\t")[0].trim() } + + devices.firstOrNull { !it.startsWith("emulator-") } ?: devices.firstOrNull() + } catch (_: Throwable) { + null + } + } +} diff --git a/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidMultiplatformTask.kt b/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidMultiplatformTask.kt index 82851f0..b637ef9 100644 --- a/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidMultiplatformTask.kt +++ b/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidMultiplatformTask.kt @@ -1,175 +1,494 @@ package org.jetbrains.compose.storytale.plugin -import com.android.build.gradle.AppExtension -import com.android.build.gradle.api.ApplicationVariant -import java.io.ByteArrayOutputStream import java.io.File -import org.gradle.api.GradleException +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.kotlin.dsl.findByType +import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.task import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget -val androidGradlePlugins = listOf("com.android.application") +val androidGradlePlugins = listOf( + "com.android.application", + "com.android.library", + "com.android.kotlin.multiplatform.library", +) -fun Project.processAndroidCompilation(extension: StorytaleExtension, target: KotlinAndroidTarget) { +fun Project.processAndroidCompilation(extension: StorytaleExtension, target: KotlinTarget) { project.logger.info("Configuring storytale for Kotlin on Android") createAndroidCompilationTasks(target, extension) } fun Project.createAndroidCompilationTasks( - target: KotlinAndroidTarget, + target: KotlinTarget, extension: StorytaleExtension, ) { + var configured = false + val configureAction = { + if (!configured) { + val isApplication = extension.project.plugins.hasPlugin("com.android.application") + val isLibrary = extension.project.plugins.hasPlugin("com.android.library") || + extension.project.plugins.hasPlugin("com.android.kotlin.multiplatform.library") + + if (isApplication) { + configured = true + configureAndroidApplication(target, extension) + } else if (isLibrary) { + configured = true + configureAndroidLibrary(target, extension) + } + } + } + androidGradlePlugins.forEach { pluginId -> extension.project.plugins.withId(pluginId) { - val storytaleBuildDir = extension.getBuildDirectory(target) - val storytaleBuildSourcesDir = file("$storytaleBuildDir/sources") - val storytaleBuildResourcesDir = file("$storytaleBuildDir/resources") - val applicationExtension = extension.project.extensions.findByType(AppExtension::class) - ?: error("Android Application plugin must be applied to the module") - - val mainStoriesSourceSet = extension.mainStoriesSourceSet - - applicationExtension.buildTypes.create(StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX) - .apply { - initWith(applicationExtension.buildTypes.getByName("debug")) - applicationIdSuffix = ".${StorytaleGradlePlugin.STORYTALE_EXEC_PREFIX}" - } + configureAction() + } + } +} + +private fun Project.configureAndroidApplication( + target: KotlinTarget, + extension: StorytaleExtension, +) { + val storytaleBuildDir = extension.getBuildDirectory(target) + val storytaleBuildSourcesDir = file("$storytaleBuildDir/sources") + val storytaleBuildResourcesDir = file("$storytaleBuildDir/resources") + val mainStoriesSourceSet = extension.mainStoriesSourceSet + + AndroidComponentsHelper.configureApplicationMinSdk(project, StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX) + + val applicationExt = project.extensions.findByType(ApplicationExtension::class.java) + if (applicationExt != null) { + val debugBuildType = applicationExt.buildTypes.findByName("debug") + applicationExt.buildTypes.maybeCreate(StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX).apply { + if (debugBuildType != null) { + initWith(debugBuildType) + } + applicationIdSuffix = ".${StorytaleGradlePlugin.STORYTALE_EXEC_PREFIX}" + val debugSigning = applicationExt.signingConfigs.findByName("debug") + if (debugSigning != null) { + signingConfig = debugSigning + } + matchingFallbacks += listOf("debug", "release") + isDebuggable = true + } + applicationExt.sourceSets.findByName(StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX)?.manifest?.srcFile( + storytaleBuildResourcesDir.resolve("AndroidManifest.xml") + ) + } else { + configureLegacyApplicationDsl(storytaleBuildResourcesDir) + } + + project.kotlinExtension.sourceSets + .matching { it.name == "android${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}" } + .configureEach { + kotlin.srcDir(storytaleBuildSourcesDir) + extension.setupCommonStoriesSourceSetDependencies(this) + mainStoriesSourceSet.kotlin.srcDirs.forEach { + kotlin.srcDir(it) + } + } - applicationExtension.sourceSets + AndroidComponentsHelper.onVariant(project, StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX) { variantName, appIdProvider -> + val generatorTask = createAndroidStorytaleGenerateSourceTask( + target, + appIdProvider, + storytaleBuildSourcesDir, + storytaleBuildResourcesDir, + ) + + tasks + .matching { + it.name.contains("Manifest", ignoreCase = true) && + (it.name.contains(variantName, ignoreCase = true) || it.name.contains("Storytale", ignoreCase = true)) + } + .configureEach { dependsOn(generatorTask) } + + tasks + .matching { it.name.endsWith("AarMetadata", ignoreCase = true) } + .configureEach { enabled = false } + + if (target is KotlinAndroidTarget) { + target.compilations .matching { it.name == StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX } .configureEach { - manifest.srcFile(storytaleBuildResourcesDir.resolve("AndroidManifest.xml")) + val debugCompilation = target.compilations.findByName("debug") + if (debugCompilation != null) { + associateWith(debugCompilation) + } + compileTaskProvider.configure { + dependsOn(generatorTask) + dependsOn(tasks.matching { it.name == "generateResourceAccessorsForCommonStories" }) + } } + } - project.kotlinExtension.sourceSets - .matching { it.name == "android${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}" } - .configureEach { - kotlin.srcDir(storytaleBuildSourcesDir) - extension.setupCommonStoriesSourceSetDependencies(this) - mainStoriesSourceSet.kotlin.srcDirs.forEach { - kotlin.srcDir(it) + val adbPath = AndroidComponentsHelper.resolveAdbPath(project) + val startEmulatorTask = createStartEmulatorTask(target, adbPath) + + val installTaskName = "install${variantName.capitalized()}" + val packageTaskName = "package${variantName.capitalized()}" + + task("${target.name}${StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX}Run") { + group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP + description = "Run Storytale gallery on Android" + notCompatibleWithConfigurationCache("Launches interactive Android application on device or emulator") + + dependsOn(startEmulatorTask) + val installTask = tasks.findByName(installTaskName) + if (installTask != null) { + dependsOn(installTask) + } else { + dependsOn(packageTaskName) + } + + doLast { + val appId = appIdProvider.get() + val activityPath = "$appId.StorytaleAppActivity" + val activeDevice = AndroidComponentsHelper.resolveActiveDeviceSerial(adbPath) + val adbCmd = if (!activeDevice.isNullOrBlank()) { + listOf(adbPath, "-s", activeDevice) + } else { + listOf(adbPath) + } + + if (installTask == null) { + val apkDir = layout.buildDirectory.asFile.get().resolve("outputs/apk/$variantName") + val apkFile = apkDir.walkTopDown().firstOrNull { it.isFile && it.extension == "apk" } + if (apkFile != null) { + logger.lifecycle("Storytale: Installing APK via $adbPath on ${activeDevice ?: "default"}...") + runProcess(*(adbCmd + listOf("install", "-r", apkFile.absolutePath)).toTypedArray()) } } - applicationExtension.applicationVariants - .matching { it.name == StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX } - .configureEach { - val generatorTask = createAndroidStorytaleGenerateSourceTask( - target, - this, - storytaleBuildSourcesDir, - storytaleBuildResourcesDir, - ) + logger.lifecycle("Storytale: Launching activity $appId/$activityPath via $adbPath (device: ${activeDevice ?: "default"})") + runProcess(*(adbCmd + listOf("shell", "am", "start", "-n", "$appId/$activityPath")).toTypedArray()) + } + } + } +} + +private fun Project.configureAndroidLibrary( + target: KotlinTarget, + extension: StorytaleExtension, +) { + project.logger.info("Configured Storytale for Android Library module '${project.path}'.") + val storytaleBuildDir = extension.getBuildDirectory(target) + val storytaleBuildSourcesDir = file("$storytaleBuildDir/sources") + val storytaleBuildResourcesDir = file("$storytaleBuildDir/resources") + val mainStoriesSourceSet = extension.mainStoriesSourceSet + + // Dynamically enable device testing on target if available + try { + val androidLib = (project.extensions.findByName("kotlin") as? org.gradle.api.plugins.ExtensionAware) + ?.extensions?.findByName("androidLibrary") + ?: (project.extensions.findByName("kotlin") as? org.gradle.api.plugins.ExtensionAware) + ?.extensions?.findByName("android") + val withDeviceTestMethod = target.javaClass.methods.firstOrNull { + it.name == "withDeviceTest" && it.parameterCount == 1 + } ?: project.extensions.findByName("android")?.javaClass?.methods?.firstOrNull { + it.name == "withDeviceTest" && it.parameterCount == 1 + } ?: androidLib?.javaClass?.methods?.firstOrNull { + it.name == "withDeviceTest" && it.parameterCount == 1 + } + if (withDeviceTestMethod != null) { + val receiver = if (androidLib != null && withDeviceTestMethod.declaringClass.isAssignableFrom(androidLib.javaClass)) { + androidLib + } else if (withDeviceTestMethod.declaringClass.isAssignableFrom(target.javaClass)) { + target + } else { + project.extensions.findByName("android") + } + if (withDeviceTestMethod.parameterTypes[0] == Function1::class.java) { + val fn: (Any) -> Unit = {} + withDeviceTestMethod.invoke(receiver, fn) + } else { + val action = Action { } + withDeviceTestMethod.invoke(receiver, action) + } + } + } catch (e: Throwable) { + project.logger.debug("Storytale: could not invoke withDeviceTest on target", e) + } + + project.kotlinExtension.sourceSets + .matching { + it.name == "androidDeviceTest" || + it.name == "androidTest" || + it.name == "android${StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX}" + } + .configureEach { + kotlin.srcDir(storytaleBuildSourcesDir) + extension.setupCommonStoriesSourceSetDependencies(this) + mainStoriesSourceSet.kotlin.srcDirs.forEach { + kotlin.srcDir(it) + } + dependencies { + implementation("androidx.activity:activity-compose:1.10.1") + } + } + + target.compilations.configureEach { + compileTaskProvider.configure { + dependsOn(tasks.matching { it.name == "generateResourceAccessorsForCommonStories" }) + } + } + + val deviceTestSourceSetName = project.kotlinExtension.sourceSets + .firstOrNull { it.name == "androidDeviceTest" || it.name == "androidTest" }?.name ?: "androidDeviceTest" + val deviceTestManifest = project.file("src/$deviceTestSourceSetName/AndroidManifest.xml") + + tasks.matching { it.name == "clean" }.configureEach { + doLast { + if (deviceTestManifest.exists() && deviceTestManifest.readText().contains(" + | + | + | + | + | + | + | + | + | + | + | + """.trimMargin() + + androidManifestFile.writeText(manifestXml) - - """.trimIndent(), - ) + deviceTestManifestFile?.let { targetFile -> + if (!targetFile.exists() || targetFile.readText().contains("