diff --git a/components/build.gradle.kts b/components/build.gradle.kts index 00461713c7..8bba7d8509 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } subprojects { - version = findProperty("deploy.version") ?: property("compose.version")!! + version = findProperty("deploy.version")!! plugins.withId("java") { configureIfExists { diff --git a/components/gradle.properties b/components/gradle.properties index 8604e6cf0e..a16ea9956e 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -8,12 +8,11 @@ android.useAndroidX=true #Versions kotlin.version=1.9.23 -compose.version=1.6.10-beta02 agp.version=8.2.2 +deploy.version=0.1.0-SNAPSHOT #Compose org.jetbrains.compose.experimental.jscanvas.enabled=true -org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.macos.enabled=true compose.desktop.verbose=true compose.useMavenLocal=false diff --git a/components/gradle/libs.versions.toml b/components/gradle/libs.versions.toml index 643996da53..6ecd0569a6 100644 --- a/components/gradle/libs.versions.toml +++ b/components/gradle/libs.versions.toml @@ -13,4 +13,6 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose" } -androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" } \ No newline at end of file +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose" } \ No newline at end of file diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index cc87a66ef0..5dc1580b80 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -52,6 +52,10 @@ kotlin { desktopMain.dependencies { implementation(compose.desktop.common) } + androidMain.dependencies { + implementation(libs.androidx.ui.tooling) + implementation(libs.androidx.ui.tooling.preview) + } val nonAndroidMain by creating { dependsOn(commonMain.get()) @@ -73,6 +77,12 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" + } } compose.experimental { diff --git a/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt b/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt index 37f0bf45d7..e61279fa17 100644 --- a/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt +++ b/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt @@ -5,9 +5,27 @@ package org.jetbrains.compose.resources.demo.shared +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.PreviewContextConfigurationEffect @Composable fun MainView() { UseResources() } + +@Preview(showBackground = true) +@Composable +fun ImagesResPreview() { + ImagesRes(PaddingValues()) +} + +@OptIn(ExperimentalResourceApi::class) +@Preview(showBackground = true) +@Composable +fun FileResPreview() { + PreviewContextConfigurationEffect() + FileRes(PaddingValues()) +} diff --git a/components/resources/library/api/android/library.api b/components/resources/library/api/android/library.api index 9c01aca1e2..0acbd5b930 100644 --- a/components/resources/library/api/android/library.api +++ b/components/resources/library/api/android/library.api @@ -1,3 +1,7 @@ +public final class org/jetbrains/compose/resources/AndroidContextProviderKt { + public static final fun PreviewContextConfigurationEffect (Landroidx/compose/runtime/Composer;I)V +} + public final class org/jetbrains/compose/resources/DensityQualifier$Companion { public final fun selectByDensity (F)Lorg/jetbrains/compose/resources/DensityQualifier; public final fun selectByValue (I)Lorg/jetbrains/compose/resources/DensityQualifier; diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 17a6d7489a..c53dcba642 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -10,8 +10,6 @@ plugins { id("org.jetbrains.kotlinx.binary-compatibility-validator") } -val composeVersion = extra["compose.version"] as String - kotlin { jvm("desktop") androidTarget { @@ -187,6 +185,7 @@ android { assets.srcDir("src/androidInstrumentedTest/assets") } named("test") { resources.srcDir(commonTestResources) } + named("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") } } } @@ -202,11 +201,6 @@ apiValidation { nonPublicMarkers.add("org.jetbrains.compose.resources.InternalResourceApi") } -// adding it here to make sure skiko is unpacked and available in web tests -compose.experimental { - web.application {} -} - //utility task to generate CLDRPluralRuleLists.kt file by 'CLDRPluralRules/plurals.xml' tasks.register("generatePluralRuleLists") { val projectDir = project.layout.projectDirectory diff --git a/components/resources/library/src/androidMain/AndroidManifest.xml b/components/resources/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..ed54901fe4 --- /dev/null +++ b/components/resources/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt new file mode 100644 index 0000000000..df9cc46c4c --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt @@ -0,0 +1,81 @@ +package org.jetbrains.compose.resources + +import android.annotation.SuppressLint +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode + +internal val androidContext get() = AndroidContextProvider.ANDROID_CONTEXT + +/** + * The function configures the android context + * to be used for non-composable resource read functions + * + * e.g. `Res.readBytes(...)` + * + * Example usage: + * ``` + * @Preview + * @Composable + * fun MyPreviewComponent() { + * PreviewContextConfigurationEffect() + * //... + * } + * ``` + */ +@ExperimentalResourceApi +@Composable +fun PreviewContextConfigurationEffect() { + if (LocalInspectionMode.current) { + AndroidContextProvider.ANDROID_CONTEXT = LocalContext.current + } +} + +//https://andretietz.com/2017/09/06/autoinitialise-android-library/ +internal class AndroidContextProvider : ContentProvider() { + companion object { + @SuppressLint("StaticFieldLeak") + var ANDROID_CONTEXT: Context? = null + } + + override fun onCreate(): Boolean { + ANDROID_CONTEXT = context + return true + } + + override fun attachInfo(context: Context, info: ProviderInfo?) { + if (info == null) { + throw NullPointerException("AndroidContextProvider ProviderInfo cannot be null.") + } + // So if the authorities equal the library internal ones, the developer forgot to set his applicationId + if ("org.jetbrains.compose.components.resources.resources.AndroidContextProvider" == info.authority) { + throw IllegalStateException("Incorrect provider authority in manifest. Most likely due to a " + + "missing applicationId variable your application\'s build.gradle.") + } + + super.attachInfo(context, info) + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 +} \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt index 009419a020..64f87e5440 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt @@ -9,5 +9,6 @@ import androidx.compose.ui.text.font.* actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val environment = LocalComposeEnvironment.current.rememberEnvironment() val path = remember(environment, resource) { resource.getResourceItemByEnvironment(environment).path } - return Font(path, LocalContext.current.assets, weight, style) + val assets = LocalContext.current.assets + return Font(path, assets, weight, style) } \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt index 972cb39205..be92557588 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt @@ -1,9 +1,21 @@ package org.jetbrains.compose.resources -import java.io.File +import android.content.res.AssetManager +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import java.io.FileNotFoundException import java.io.InputStream internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + private val assets: AssetManager by lazy { + val context = androidContext ?: error( + "Android context is not initialized. " + + "If it happens in the Preview mode then call PreviewContextConfigurationEffect() function." + ) + context.assets + } + override suspend fun read(path: String): ByteArray { val resource = getResourceAsStream(path) return resource.readBytes() @@ -33,39 +45,52 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou private fun InputStream.readBytes(byteArray: ByteArray, offset: Int, size: Int) { var readBytes = 0 while (readBytes < size) { - val count = read(byteArray, offset + readBytes, size - readBytes) + val count = read(byteArray, offset + readBytes, size - readBytes) if (count <= 0) break readBytes += count } } override fun getUri(path: String): String { - val classLoader = getClassLoader() - val resource = classLoader.getResource(path) ?: run { - //try to find a font in the android assets - if (File(path).isFontResource()) { - classLoader.getResource("assets/$path") - } else null - } ?: throw MissingResourceException(path) - return resource.toURI().toString() + val uri = if (assets.hasFile(path)) { + Uri.parse("file:///android_asset/$path") + } else { + val classLoader = getClassLoader() + val resource = classLoader.getResource(path) ?: throw MissingResourceException(path) + resource.toURI() + } + return uri.toString() } private fun getResourceAsStream(path: String): InputStream { - val classLoader = getClassLoader() - val resource = classLoader.getResourceAsStream(path) ?: run { - //try to find a font in the android assets - if (File(path).isFontResource()) { - classLoader.getResourceAsStream("assets/$path") - } else null - } ?: throw MissingResourceException(path) - return resource - } - - private fun File.isFontResource(): Boolean { - return this.parentFile?.name.orEmpty().startsWith("font") + return try { + assets.open(path) + } catch (e: FileNotFoundException) { + val classLoader = getClassLoader() + classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) + } } private fun getClassLoader(): ClassLoader { return this.javaClass.classLoader ?: error("Cannot find class loader") } -} \ No newline at end of file + + private fun AssetManager.hasFile(path: String): Boolean { + var inputStream: InputStream? = null + val result = try { + inputStream = open(path) + true + } catch (e: FileNotFoundException) { + false + } finally { + inputStream?.close() + } + return result + } +} + +internal actual val ProvidableCompositionLocal.currentOrPreview: ResourceReader + @Composable get() { + PreviewContextConfigurationEffect() + return current + } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt index 96f653d87e..30892cf43f 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @@ -55,7 +55,7 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } */ @Composable fun imageResource(resource: DrawableResource): ImageBitmap { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val imageBitmap by rememberResourceState(resource, resourceReader, { emptyImageBitmap }) { env -> val path = resource.getResourceItemByEnvironment(env).path val cached = loadImage(path, resourceReader) { @@ -78,7 +78,7 @@ private val emptyImageVector: ImageVector by lazy { */ @Composable fun vectorResource(resource: DrawableResource): ImageVector { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val density = LocalDensity.current val imageVector by rememberResourceState(resource, resourceReader, density, { emptyImageVector }) { env -> val path = resource.getResourceItemByEnvironment(env).path @@ -98,7 +98,7 @@ private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) } @Composable private fun svgPainter(resource: DrawableResource): Painter { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val density = LocalDensity.current val svgPainter by rememberResourceState(resource, resourceReader, density, { emptySvgPainter }) { env -> val path = resource.getResourceItemByEnvironment(env).path diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt index 27be9337f5..fa12b8c11b 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt @@ -26,7 +26,7 @@ class PluralStringResource */ @Composable fun pluralStringResource(resource: PluralStringResource, quantity: Int): String { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val pluralStr by rememberResourceState(resource, quantity, { "" }) { env -> loadPluralString(resource, quantity, resourceReader, env) } @@ -93,7 +93,7 @@ private suspend fun loadPluralString( */ @Composable fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val args = formatArgs.map { it.toString() } val pluralStr by rememberResourceState(resource, quantity, args, { "" }) { env -> loadPluralString(resource, quantity, args, resourceReader, env) diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt index 7c2979e586..9c7bf1b4d5 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt @@ -1,5 +1,7 @@ package org.jetbrains.compose.resources +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf class MissingResourceException(path: String) : Exception("Missing resource with path: $path") @@ -34,3 +36,7 @@ internal val DefaultResourceReader = getPlatformResourceReader() //ResourceReader provider will be overridden for tests internal val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader } + +//For an android preview we need to initialize the resource reader with the local context +internal expect val ProvidableCompositionLocal.currentOrPreview: ResourceReader + @Composable get diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt index e89aa5c600..2077db5531 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt @@ -30,7 +30,7 @@ class StringArrayResource */ @Composable fun stringArrayResource(resource: StringArrayResource): List { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val array by rememberResourceState(resource, { emptyList() }) { env -> loadStringArray(resource, resourceReader, env) } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt index 3cce39b722..6f44a23b89 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @@ -23,7 +23,7 @@ class StringResource */ @Composable fun stringResource(resource: StringResource): String { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val str by rememberResourceState(resource, { "" }) { env -> loadString(resource, resourceReader, env) } @@ -75,7 +75,7 @@ private suspend fun loadString( */ @Composable fun stringResource(resource: StringResource, vararg formatArgs: Any): String { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val args = formatArgs.map { it.toString() } val str by rememberResourceState(resource, args, { "" }) { env -> loadString(resource, args, resourceReader, env) diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index 712e8a8cbf..007f23bcef 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -293,7 +293,7 @@ class ComposeResourceTest { var uri2 = "" setContent { CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview uri1 = resourceReader.getUri("1.png") uri2 = resourceReader.getUri("2.png") } diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index 5390361c63..c5e9e1682e 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -31,7 +31,7 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { - val resourceReader = LocalResourceReader.current + val resourceReader = LocalResourceReader.currentOrPreview val fontFile by rememberResourceState(resource, weight, style, { defaultEmptyFont }) { env -> val path = resource.getResourceItemByEnvironment(env).path val fontBytes = resourceReader.read(path) diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt new file mode 100644 index 0000000000..17d29d49ef --- /dev/null +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt @@ -0,0 +1,7 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal + +internal actual val ProvidableCompositionLocal.currentOrPreview: ResourceReader + @Composable get() = current \ No newline at end of file diff --git a/components/settings.gradle.kts b/components/settings.gradle.kts index 060807a189..d7283e752d 100644 --- a/components/settings.gradle.kts +++ b/components/settings.gradle.kts @@ -12,10 +12,12 @@ pluginManagement { plugins { kotlin("jvm").version(extra["kotlin.version"] as String) kotlin("multiplatform").version(extra["kotlin.version"] as String) - id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("org.jetbrains.compose") //version is not required because the plugin is included to the build id("com.android.library").version(extra["agp.version"] as String) id("org.jetbrains.kotlinx.binary-compatibility-validator").version("0.15.0-Beta.2") } + + includeBuild("../gradle-plugins") } dependencyResolutionManagement { diff --git a/components/ui-tooling-preview/library/build.gradle.kts b/components/ui-tooling-preview/library/build.gradle.kts index 964b992ea0..1007eadc3b 100644 --- a/components/ui-tooling-preview/library/build.gradle.kts +++ b/components/ui-tooling-preview/library/build.gradle.kts @@ -7,8 +7,6 @@ plugins { id("com.android.library") } -val composeVersion = extra["compose.version"] as String - kotlin { jvm("desktop") androidTarget { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt index f1d9f2855b..8e98561fb9 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt @@ -1,7 +1,6 @@ package org.jetbrains.compose.resources import com.android.build.api.variant.AndroidComponentsExtension -import com.android.build.gradle.BaseExtension import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask import com.android.build.gradle.internal.lint.LintModelWriterTask import org.gradle.api.DefaultTask @@ -10,49 +9,23 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.file.FileSystemOperations import org.gradle.api.provider.Property -import org.gradle.api.tasks.* +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction import org.jetbrains.compose.internal.utils.registerTask import org.jetbrains.compose.internal.utils.uppercaseFirstChar -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation -import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull import org.jetbrains.kotlin.gradle.utils.ObservableSet import javax.inject.Inject -@OptIn(ExperimentalKotlinGradlePluginApi::class) internal fun Project.configureAndroidComposeResources( - kotlinExtension: KotlinMultiplatformExtension, - androidExtension: BaseExtension + kotlinExtension: KotlinMultiplatformExtension ) { - // 1) get the Kotlin Android Target Compilation -> [A] - // 2) get default source set name for the 'A' - // 3) find the associated Android SourceSet in the AndroidExtension -> [B] - // 4) get all source sets in the 'A' and add its resources to the 'B' - kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget -> - androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation -> - compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet -> - androidExtension.sourceSets - .matching { it.name == kotlinAndroidSourceSet.androidSourceSetName } - .all { androidSourceSet -> - (compilation.allKotlinSourceSets as? ObservableSet)?.forAll { kotlinSourceSet -> - val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet) - androidSourceSet.resources.srcDirs(preparedComposeResources) - - //fix for AGP < 8.0 - //usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough - compilation.androidVariant.processJavaResourcesProvider.configure { - it.dependsOn(preparedComposeResources) - } - } - } - } - } - } - - //copy fonts from the compose resources dir to android assets + //copy all compose resources to android assets val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return androidComponents.onVariants { variant -> val variantResources = project.files() @@ -60,7 +33,7 @@ internal fun Project.configureAndroidComposeResources( kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget -> androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation -> if (compilation.androidVariant.name == variant.name) { - project.logger.info("Configure fonts for variant ${variant.name}") + project.logger.info("Configure resources for variant ${variant.name}") (compilation.allKotlinSourceSets as? ObservableSet)?.forAll { kotlinSourceSet -> val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet) variantResources.from(preparedComposeResources) @@ -69,22 +42,32 @@ internal fun Project.configureAndroidComposeResources( } } - val copyFonts = registerTask( - "copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets" + val copyResources = registerTask( + "copy${variant.name.uppercaseFirstChar()}ResourcesToAndroidAssets" ) { from.set(variantResources) } - variant.sources?.assets?.addGeneratedSourceDirectory( - taskProvider = copyFonts, - wiredWith = CopyAndroidFontsToAssetsTask::outputDirectory - ) - //exclude a duplication of fonts in apks - variant.packaging.resources.excludes.add("**/font*/*") + variant.sources.assets?.apply { + addGeneratedSourceDirectory( + taskProvider = copyResources, + wiredWith = CopyResourcesToAndroidAssetsTask::outputDirectory + ) + + // addGeneratedSourceDirectory doesn't mark the output directory as assets hence AS Compose Preview doesn't work + addStaticSourceDirectory(copyResources.flatMap { it.outputDirectory.asFile }.get().path) + + // addGeneratedSourceDirectory doesn't run the copyResources task during AS Compose Preview build + tasks.configureEach { task -> + if (task.name == "compile${variant.name.uppercaseFirstChar()}Sources") { + task.dependsOn(copyResources) + } + } + } } } //Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API -internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() { +internal abstract class CopyResourcesToAndroidAssetsTask : DefaultTask() { @get:Inject abstract val fileSystem: FileSystemOperations @@ -100,7 +83,6 @@ internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() { fileSystem.copy { it.includeEmptyDirs = false it.from(from) - it.include("**/font*/*") it.into(outputDirectory) } } @@ -121,5 +103,31 @@ internal fun Project.fixAndroidLintTaskDependencies() { it is AndroidLintAnalysisTask || it is LintModelWriterTask }.configureEach { it.mustRunAfter(tasks.withType(GenerateResourceAccessorsTask::class.java)) + it.mustRunAfter(tasks.withType(CopyResourcesToAndroidAssetsTask::class.java)) + } +} + +internal fun Project.configureAndroidAssetsForPreview() { + val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return + androidComponents.onVariants { variant -> + variant.sources.assets?.apply { + val kgpCopyAssetsTaskName = "${variant.name}AssetsCopyForAGP" + + // addGeneratedSourceDirectory doesn't mark the output directory as assets hence AS Compose Preview doesn't work + tasks.all { task -> + if (task.name == kgpCopyAssetsTaskName) { + task.outputs.files.forEach { file -> + addStaticSourceDirectory(file.path) + } + } + } + + // addGeneratedSourceDirectory doesn't run the copyResources task during AS Compose Preview build + tasks.configureEach { task -> + if (task.name == "compile${variant.name.uppercaseFirstChar()}Sources") { + task.dependsOn(kgpCopyAssetsTaskName) + } + } + } } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt index 4b67866036..7b7ba44529 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt @@ -1,24 +1,17 @@ package org.jetbrains.compose.resources -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask -import com.android.build.gradle.internal.lint.LintModelWriterTask import org.gradle.api.Project import org.gradle.api.provider.Provider import org.gradle.api.tasks.SourceSet -import org.gradle.api.tasks.TaskProvider import org.gradle.util.GradleVersion -import org.jetbrains.compose.ComposePlugin import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin -import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.extraProperties -import java.io.File internal const val COMPOSE_RESOURCES_DIR = "composeResources" internal const val RES_GEN_DIR = "generated/compose/resourceGenerator" @@ -46,7 +39,10 @@ private fun Project.onKgpApplied(config: Provider, kgp: Kotl if (kmpResourcesAreAvailable) { configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, config) - onAgpApplied { fixAndroidLintTaskDependencies() } + onAgpApplied { + configureAndroidAssetsForPreview() + fixAndroidLintTaskDependencies() + } } else { if (!disableMultimoduleResources) { if (!hasKmpResources) logger.info( @@ -66,8 +62,8 @@ private fun Project.onKgpApplied(config: Provider, kgp: Kotl val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME configureComposeResources(kotlinExtension, commonMain, config) - onAgpApplied { androidExtension -> - configureAndroidComposeResources(kotlinExtension, androidExtension) + onAgpApplied { + configureAndroidComposeResources(kotlinExtension) fixAndroidLintTaskDependencies() } } @@ -75,11 +71,10 @@ private fun Project.onKgpApplied(config: Provider, kgp: Kotl configureSyncIosComposeResources(kotlinExtension) } -private fun Project.onAgpApplied(block: (androidExtension: BaseExtension) -> Unit) { +private fun Project.onAgpApplied(block: () -> Unit) { androidPluginIds.forEach { pluginId -> plugins.withId(pluginId) { - val androidExtension = project.extensions.getByType(BaseExtension::class.java) - block(androidExtension) + block() } } } @@ -90,8 +85,6 @@ private fun Project.onKotlinJvmApplied(config: Provider) { configureComposeResources(kotlinExtension, main, config) } -// sourceSet.resources.srcDirs doesn't work for Android targets. -// Android resources should be configured separately private fun Project.configureComposeResources( kotlinExtension: KotlinProjectExtension, resClassSourceSetName: String, @@ -100,7 +93,16 @@ private fun Project.configureComposeResources( logger.info("Configure compose resources") configureComposeResourcesGeneration(kotlinExtension, resClassSourceSetName, config, false) + // mark prepared resources as sourceSet.resources + // 1) it automatically packs the resources to JVM jars + // 2) it configures the webpack to use the resources + // 3) for native targets we will use source set resources to pack them into the final app. see IosResources.kt + // 4) for the android it DOESN'T pack resources! we copy resources to assets in AndroidResources.kt kotlinExtension.sourceSets.all { sourceSet -> - sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet)) + // the HACK is here because KGP copy androidMain java resources to Android target + // if the resources were registered in the androidMain source set before the target declaration + afterEvaluate { + sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet)) + } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt index 3a945db560..5ace0c22a9 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt @@ -32,28 +32,26 @@ internal fun Project.configureKmpResources( logger.info("Configure resources publication for '${target.targetName}' target") val packedResourceDir = config.getModuleResourcesDir(project) - kmpResources.publishResourcesAsKotlinComponent( - target, - { sourceSet -> - KotlinTargetResourcesPublication.ResourceRoot( - getPreparedComposeResourcesDir(sourceSet), - emptyList(), - //for android target exclude fonts - if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList() - ) - }, - packedResourceDir - ) - - if (target is KotlinAndroidTarget) { - //for android target publish fonts in assets - logger.info("Configure fonts relocation for '${target.targetName}' target") + if (target !is KotlinAndroidTarget) { + kmpResources.publishResourcesAsKotlinComponent( + target, + { sourceSet -> + KotlinTargetResourcesPublication.ResourceRoot( + getPreparedComposeResourcesDir(sourceSet), + emptyList(), + emptyList() + ) + }, + packedResourceDir + ) + } else { + //for android target publish resources in assets kmpResources.publishInAndroidAssets( target, { sourceSet -> KotlinTargetResourcesPublication.ResourceRoot( getPreparedComposeResourcesDir(sourceSet), - listOf("**/font*/*"), + emptyList(), emptyList() ) }, diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt index 9b5c8b22ae..53a9e5b822 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt @@ -251,32 +251,13 @@ class ResourcesTest : GradlePluginTestBase() { "my-mvn/me/sample/library/cmplib-$target/1.0/cmplib-$target-1.0$ext" val aar = file(libpath("android", ".aar")) - val innerClassesJar = aar.parentFile.resolve("aar-inner-classes.jar") assertTrue(aar.exists(), "File not found: " + aar.path) - ZipFile(aar).use { zip -> - resourcesFiles - .filter { it.startsWith("font") } - .forEach { fontRes -> + ZipFile(aar).use { zip -> resourcesFiles.forEach { fontRes -> assertNotNull( zip.getEntry("assets/composeResources/$subdir/$fontRes"), "Resource not found: '$fontRes' in aar '${aar.path}'" ) - } - - innerClassesJar.writeBytes( - zip.getInputStream(zip.getEntry("classes.jar")).readBytes() - ) - } - ZipFile(innerClassesJar).use { zip -> - resourcesFiles - .filterNot { it.startsWith("font") } - .forEach { res -> - assertNotNull( - zip.getEntry("composeResources/$subdir/$res"), - "Resource not found: '$res' in aar/classes.jar '${aar.path}'" - ) - } - } + } } val jar = file(libpath("jvm", ".jar")) checkResourcesZip(jar, resourcesFiles, subdir) @@ -393,37 +374,37 @@ class ResourcesTest : GradlePluginTestBase() { .getConvertedResources(commonResourcesDir) gradle("build").checks { - check.taskSuccessful(":copyDemoDebugFontsToAndroidAssets") - check.taskSuccessful(":copyDemoReleaseFontsToAndroidAssets") - check.taskSuccessful(":copyFullDebugFontsToAndroidAssets") - check.taskSuccessful(":copyFullReleaseFontsToAndroidAssets") + check.taskSuccessful(":copyDemoDebugResourcesToAndroidAssets") + check.taskSuccessful(":copyDemoReleaseResourcesToAndroidAssets") + check.taskSuccessful(":copyFullDebugResourcesToAndroidAssets") + check.taskSuccessful(":copyFullReleaseResourcesToAndroidAssets") getAndroidApk("demo", "debug", "Resources-Test").let { apk -> checkResourcesInZip(apk, commonResourcesFiles, true) assertEquals( "android demo-debug", - readFileInZip(apk, "files/platform.txt").decodeToString() + readFileInZip(apk, "assets/files/platform.txt").decodeToString() ) } getAndroidApk("demo", "release", "Resources-Test").let { apk -> checkResourcesInZip(apk, commonResourcesFiles, true) assertEquals( "android demo-release", - readFileInZip(apk, "files/platform.txt").decodeToString() + readFileInZip(apk, "assets/files/platform.txt").decodeToString() ) } getAndroidApk("full", "debug", "Resources-Test").let { apk -> checkResourcesInZip(apk, commonResourcesFiles, true) assertEquals( "android full-debug", - readFileInZip(apk, "files/platform.txt").decodeToString() + readFileInZip(apk, "assets/files/platform.txt").decodeToString() ) } getAndroidApk("full", "release", "Resources-Test").let { apk -> checkResourcesInZip(apk, commonResourcesFiles, true) assertEquals( "android full-release", - readFileInZip(apk, "files/platform.txt").decodeToString() + readFileInZip(apk, "assets/files/platform.txt").decodeToString() ) } @@ -443,36 +424,6 @@ class ResourcesTest : GradlePluginTestBase() { } } - @Test - fun testAndroidFonts(): Unit = with(testProject("misc/commonResources")) { - val commonResourcesDir = file("src/commonMain/composeResources") - val commonResourcesFiles = commonResourcesDir.walkTopDown() - .filter { !it.isDirectory && !it.isHidden } - .getConvertedResources(commonResourcesDir) - - gradle("assembleDebug").checks { - check.taskSuccessful(":copyDebugFontsToAndroidAssets") - - getAndroidApk("", "debug", "Resources-Test").let { apk -> - checkResourcesInZip(apk, commonResourcesFiles, true) - } - } - - file("src/commonMain/composeResources/font-en").renameTo( - file("src/commonMain/composeResources/font-mdpi") - ) - val newCommonResourcesFiles = commonResourcesDir.walkTopDown() - .filter { !it.isDirectory && !it.isHidden } - .getConvertedResources(commonResourcesDir) - gradle("assembleDebug").checks { - check.taskSuccessful(":copyDebugFontsToAndroidAssets") - - getAndroidApk("", "debug", "Resources-Test").let { apk -> - checkResourcesInZip(apk, newCommonResourcesFiles, true) - } - } - } - private fun Sequence.getConvertedResources(baseDir: File) = map { file -> val newFile = if ( file.parentFile.name.startsWith("value") && @@ -486,7 +437,6 @@ class ResourcesTest : GradlePluginTestBase() { newFile.relativeTo(baseDir).invariantSeparatorsPath } - private fun File.writeNewFile(text: String) { parentFile.mkdirs() createNewFile() @@ -507,8 +457,8 @@ class ResourcesTest : GradlePluginTestBase() { ZipFile(file).use { zip -> commonResourcesFiles.forEach { res -> println("check '$res' file") - if (isAndroid && res.startsWith("font")) { - //android fonts should be only in assets + if (isAndroid) { + //android resources should be only in assets assertNull(zip.getEntry(res), "file = '$res'") assertNotNull(zip.getEntry("assets/$res"), "file = 'assets/$res'") } else {