Skip to content

Commit

Permalink
Pack all resources to assets on the android target. (#4965)
Browse files Browse the repository at this point in the history
The PR changes the android resources packaging. Now all resources are
packed to the android assets (not only fonts). It unblocks usage android
URIs to the resources in a WebView or other external resource consumers.
Additionally the PR fixes Android Studio Compose Previews work with
multiplatform resources:


![](https://private-user-images.githubusercontent.com/3532155/341182790-ef26b667-ad0d-4efd-b7f9-23cff92ab49d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTg4Nzg0MTgsIm5iZiI6MTcxODg3ODExOCwicGF0aCI6Ii8zNTMyMTU1LzM0MTE4Mjc5MC1lZjI2YjY2Ny1hZDBkLTRlZmQtYjdmOS0yM2NmZjkyYWI0OWQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYyMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MjBUMTAwODM4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9OTY1MzdhMTAxMjNmZDRhMDA4ZjdjODBjYzg3M2MyNDg0ZTA5OWFkZGZkZjk1ZDUwOWFkZDk3MmQ2YjIzNzJiYiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.xgUAr_2--ZHo6txhdAANRbe8ju2SQ5EACvK96gaGJnY)

For a backward compatibility the resources library tries to read
resources in java resources if assets were not found.

Fixes #4877
Fixes #4503
Fixes #4932
Fixes #4476

## Release Notes

### Features - Resources
- Android Studio Preview works with Compose Multiplatform resources now
- Compose Multiplatform resources are stored in the android assets now.
This fixes such cases as a rendering resource files in WebViews or Media
Players
  • Loading branch information
terrakok authored Jun 20, 2024
1 parent 5c141b5 commit 8fc3dd2
Show file tree
Hide file tree
Showing 25 changed files with 305 additions and 187 deletions.
2 changes: 1 addition & 1 deletion components/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

subprojects {
version = findProperty("deploy.version") ?: property("compose.version")!!
version = findProperty("deploy.version")!!

plugins.withId("java") {
configureIfExists<JavaPluginExtension> {
Expand Down
3 changes: 1 addition & 2 deletions components/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion components/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
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" }
10 changes: 10 additions & 0 deletions components/resources/demo/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -73,6 +77,12 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.11"
}
}

compose.experimental {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
4 changes: 4 additions & 0 deletions components/resources/library/api/android/library.api
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
8 changes: 1 addition & 7 deletions components/resources/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ plugins {
id("org.jetbrains.kotlinx.binary-compatibility-validator")
}

val composeVersion = extra["compose.version"] as String

kotlin {
jvm("desktop")
androidTarget {
Expand Down Expand Up @@ -187,6 +185,7 @@ android {
assets.srcDir("src/androidInstrumentedTest/assets")
}
named("test") { resources.srcDir(commonTestResources) }
named("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") }
}
}

Expand All @@ -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<GeneratePluralRuleListsTask>("generatePluralRuleLists") {
val projectDir = project.layout.projectDirectory
Expand Down
13 changes: 13 additions & 0 deletions components/resources/library/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<provider
android:authorities="${applicationId}.resources.AndroidContextProvider"
android:name="org.jetbrains.compose.resources.AndroidContextProvider"
android:exported="false"
android:enabled="true">
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
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<out String>?): Int = 0
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int = 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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")
}
}

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<ResourceReader>.currentOrPreview: ResourceReader
@Composable get() {
PreviewContextConfigurationEffect()
return current
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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<ResourceReader>.currentOrPreview: ResourceReader
@Composable get
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class StringArrayResource
*/
@Composable
fun stringArrayResource(resource: StringArrayResource): List<String> {
val resourceReader = LocalResourceReader.current
val resourceReader = LocalResourceReader.currentOrPreview
val array by rememberResourceState(resource, { emptyList() }) { env ->
loadStringArray(resource, resourceReader, env)
}
Expand Down
Loading

0 comments on commit 8fc3dd2

Please sign in to comment.