Skip to content

Commit 628a6a4

Browse files
committed
feat(andorid): implement referencedAssets
1 parent 2d35e25 commit 628a6a4

3 files changed

Lines changed: 271 additions & 17 deletions

File tree

android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.facebook.proguard.annotations.DoNotStrip
88
@DoNotStrip
99
class HybridRiveFile : HybridRiveFileSpec() {
1010
var riveFile: File? = null
11+
var referencedAssetCache: ReferencedAssetCache? = null
1112

1213
override val viewModelCount: Double?
1314
get() = riveFile?.viewModelCount?.toDouble()
@@ -37,8 +38,14 @@ class HybridRiveFile : HybridRiveFileSpec() {
3738
}
3839
}
3940

41+
override fun updateReferencedAssets(referencedAssets: ReferencedAssetsType) {
42+
// TODO: Implement dynamic asset updates
43+
}
44+
4045
override fun release() {
4146
riveFile?.release()
4247
riveFile = null
48+
referencedAssetCache?.clear()
49+
referencedAssetCache = null
4350
}
4451
}

android/src/main/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.margelo.nitro.rive
33
import android.annotation.SuppressLint
44
import androidx.annotation.Keep
55
import app.rive.runtime.kotlin.core.File
6+
import app.rive.runtime.kotlin.core.Rive
67
import com.facebook.proguard.annotations.DoNotStrip
78
import com.margelo.nitro.core.ArrayBuffer
89
import com.margelo.nitro.core.Promise
@@ -13,29 +14,53 @@ import java.io.File as JavaFile
1314
import java.net.URI
1415
import java.net.URL
1516

17+
data class FileAndCache(
18+
val file: File,
19+
val cache: ReferencedAssetCache
20+
)
21+
1622
@Keep
1723
@DoNotStrip
1824
class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
19-
override fun fromURL(url: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
25+
private val assetLoader = ReferencedAssetLoader()
26+
27+
private fun buildRiveFile(
28+
data: ByteArray,
29+
referencedAssets: ReferencedAssetsType?
30+
): FileAndCache {
31+
val cache = mutableMapOf<String, app.rive.runtime.kotlin.core.FileAsset>()
32+
val customLoader = assetLoader.createCustomLoader(referencedAssets, cache)
33+
34+
// TODO: The File object in Android does not have the concept of loading CDN assets
35+
val riveFile = if (customLoader != null) {
36+
File(data, Rive.defaultRendererType, customLoader)
37+
} else {
38+
File(data)
39+
}
40+
41+
return FileAndCache(riveFile, cache)
42+
}
43+
44+
override fun fromURL(url: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
2045
return Promise.async {
2146
try {
22-
val riveFile = withContext(Dispatchers.IO) {
47+
val fileAndCache = withContext(Dispatchers.IO) {
2348
val urlObj = URL(url)
2449
val riveData = urlObj.readBytes()
25-
// TODO: The File object in Android does not have the concept of loading CDN assets
26-
File(riveData)
50+
buildRiveFile(riveData, referencedAssets)
2751
}
2852

2953
val hybridRiveFile = HybridRiveFile()
30-
hybridRiveFile.riveFile = riveFile
54+
hybridRiveFile.riveFile = fileAndCache.file
55+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
3156
hybridRiveFile
3257
} catch (e: Exception) {
3358
throw Error("Failed to download Rive file: ${e.message}")
3459
}
3560
}
3661
}
3762

38-
override fun fromFileURL(fileURL: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
63+
override fun fromFileURL(fileURL: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
3964
if (!fileURL.startsWith("file://")) {
4065
throw Error("fromFileURL: URL must be a file URL: $fileURL")
4166
}
@@ -45,14 +70,15 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
4570
val uri = URI(fileURL)
4671
val path = uri.path ?: throw Error("fromFileURL: Invalid URL: $fileURL")
4772

48-
val riveFile = withContext(Dispatchers.IO) {
73+
val fileAndCache = withContext(Dispatchers.IO) {
4974
val file = JavaFile(path)
5075
val riveData = file.readBytes()
51-
File(riveData)
76+
buildRiveFile(riveData, referencedAssets)
5277
}
5378

5479
val hybridRiveFile = HybridRiveFile()
55-
hybridRiveFile.riveFile = riveFile
80+
hybridRiveFile.riveFile = fileAndCache.file
81+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
5682
hybridRiveFile
5783
} catch (e: Exception) {
5884
throw Error("Failed to load Rive file: ${e.message}")
@@ -61,39 +87,41 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
6187
}
6288

6389
@SuppressLint("DiscouragedApi")
64-
override fun fromResource(resource: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
90+
override fun fromResource(resource: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
6591
return Promise.async {
6692
try {
6793
val context = NitroModules.applicationContext
6894
?: throw Error("Could not load Rive file ($resource) from resource. No application context.")
69-
val riveFile = withContext(Dispatchers.IO) {
95+
val fileAndCache = withContext(Dispatchers.IO) {
7096
val resourceId = context.resources.getIdentifier(resource, "raw", context.packageName)
7197
if (resourceId == 0) {
7298
throw Error("Could not find Rive file: $resource.riv")
7399
}
74100
val inputStream = context.resources.openRawResource(resourceId)
75101
val riveData = inputStream.readBytes()
76-
File(riveData)
102+
buildRiveFile(riveData, referencedAssets)
77103
}
78104

79105
val hybridRiveFile = HybridRiveFile()
80-
hybridRiveFile.riveFile = riveFile
106+
hybridRiveFile.riveFile = fileAndCache.file
107+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
81108
hybridRiveFile
82109
} catch (e: Exception) {
83110
throw Error("Failed to load Rive file: ${e.message}")
84111
}
85112
}
86113
}
87114

88-
override fun fromBytes(bytes: ArrayBuffer, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
89-
val buffer = bytes.getBuffer(false) // Use false to avoid creating a read-only buffer
115+
override fun fromBytes(bytes: ArrayBuffer, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
116+
val buffer = bytes.getBuffer(false)
90117
return Promise.async {
91118
try {
92119
val byteArray = ByteArray(buffer.remaining())
93120
buffer.get(byteArray)
94-
val riveFile = File(byteArray)
121+
val fileAndCache = buildRiveFile(byteArray, referencedAssets)
95122
val hybridRiveFile = HybridRiveFile()
96-
hybridRiveFile.riveFile = riveFile
123+
hybridRiveFile.riveFile = fileAndCache.file
124+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
97125
hybridRiveFile
98126
} catch (e: Exception) {
99127
throw Error("Failed to load Rive file from bytes: ${e.message}")
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package com.margelo.nitro.rive
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.content.res.Resources
6+
import android.net.Uri
7+
import android.util.Log
8+
import app.rive.runtime.kotlin.core.*
9+
import com.margelo.nitro.NitroModules
10+
import kotlinx.coroutines.*
11+
import java.io.File as JavaFile
12+
import java.io.IOException
13+
import java.net.URI
14+
import java.net.URL
15+
16+
typealias ReferencedAssetCache = MutableMap<String, FileAsset>
17+
18+
class ReferencedAssetLoader {
19+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
20+
21+
private fun logError(message: String) {
22+
Log.e("ReferencedAssetLoader", message)
23+
}
24+
25+
private fun isValidUrl(url: String): Boolean {
26+
return try {
27+
URL(url)
28+
true
29+
} catch (e: Exception) {
30+
false
31+
}
32+
}
33+
34+
private fun constructFilePath(filename: String, path: String): String {
35+
return if (path.endsWith("/")) "$path$filename" else "$path/$filename"
36+
}
37+
38+
@SuppressLint("DiscouragedApi")
39+
private fun getResourceId(source: String, context: Context): Int {
40+
val resourceTypes = listOf("raw", "drawable")
41+
42+
for (type in resourceTypes) {
43+
val resourceId = context.resources.getIdentifier(source, type, context.packageName)
44+
if (resourceId != 0) {
45+
return resourceId
46+
}
47+
}
48+
49+
return 0
50+
}
51+
52+
private fun readAssetBytes(context: Context, fileName: String): ByteArray? {
53+
val assetManager = context.assets
54+
return try {
55+
assetManager.open(fileName).use { inputStream ->
56+
inputStream.readBytes()
57+
}
58+
} catch (e: IOException) {
59+
logError("Unable to read file from assets: $fileName - ${e.message}")
60+
null
61+
}
62+
}
63+
64+
private fun downloadUrlAsset(url: String, listener: (ByteArray) -> Unit) {
65+
if (!isValidUrl(url)) {
66+
logError("Invalid URL: $url")
67+
return
68+
}
69+
70+
scope.launch {
71+
try {
72+
val uri = URI(url)
73+
val bytes = when (uri.scheme) {
74+
"file" -> {
75+
val file = JavaFile(uri.path)
76+
if (!file.exists()) {
77+
throw IOException("File not found: ${uri.path}")
78+
}
79+
if (!file.canRead()) {
80+
throw IOException("Permission denied: ${uri.path}")
81+
}
82+
file.readBytes()
83+
}
84+
"http", "https" -> {
85+
URL(url).readBytes()
86+
}
87+
else -> {
88+
logError("Unsupported URL scheme: ${uri.scheme}")
89+
return@launch
90+
}
91+
}
92+
93+
withContext(Dispatchers.Main) {
94+
listener(bytes)
95+
}
96+
} catch (e: Exception) {
97+
logError("Unable to download asset from URL: $url - ${e.message}")
98+
}
99+
}
100+
}
101+
102+
private fun loadResourceAsset(
103+
sourceAssetId: String,
104+
context: Context,
105+
listener: (ByteArray) -> Unit
106+
) {
107+
scope.launch {
108+
try {
109+
val scheme = runCatching { Uri.parse(sourceAssetId).scheme }.getOrNull()
110+
111+
if (scheme != null) {
112+
downloadUrlAsset(sourceAssetId, listener)
113+
return@launch
114+
}
115+
116+
val resourceId = getResourceId(sourceAssetId, context)
117+
118+
if (resourceId != 0) {
119+
val bytes = context.resources.openRawResource(resourceId).use { inputStream ->
120+
inputStream.readBytes()
121+
}
122+
withContext(Dispatchers.Main) {
123+
listener(bytes)
124+
}
125+
} else {
126+
logError("Resource not found: $sourceAssetId")
127+
}
128+
} catch (e: IOException) {
129+
logError("IO Exception while reading resource: $sourceAssetId - ${e.message}")
130+
} catch (e: Resources.NotFoundException) {
131+
logError("Resource not found: $sourceAssetId - ${e.message}")
132+
} catch (e: Exception) {
133+
logError("Unexpected error while processing resource: $sourceAssetId - ${e.message}")
134+
}
135+
}
136+
}
137+
138+
private fun loadBundledAsset(
139+
sourceAsset: String,
140+
path: String?,
141+
context: Context,
142+
listener: (ByteArray) -> Unit
143+
) {
144+
scope.launch {
145+
try {
146+
val fullPath = if (path == null) sourceAsset else constructFilePath(sourceAsset, path)
147+
val bytes = readAssetBytes(context, fullPath)
148+
149+
if (bytes != null) {
150+
withContext(Dispatchers.Main) {
151+
listener(bytes)
152+
}
153+
}
154+
} catch (e: Exception) {
155+
logError("Error loading bundled asset: $sourceAsset - ${e.message}")
156+
}
157+
}
158+
}
159+
160+
private fun processAssetBytes(bytes: ByteArray, asset: FileAsset) {
161+
when (asset) {
162+
is ImageAsset -> asset.image = RiveRenderImage.make(bytes)
163+
is FontAsset -> asset.font = RiveFont.make(bytes)
164+
is AudioAsset -> asset.audio = RiveAudio.make(bytes)
165+
}
166+
}
167+
168+
private fun loadAsset(assetData: ResolvedReferencedAsset, asset: FileAsset, context: Context) {
169+
val listener: (ByteArray) -> Unit = { bytes ->
170+
processAssetBytes(bytes, asset)
171+
}
172+
173+
when {
174+
assetData.sourceAssetId != null -> {
175+
loadResourceAsset(assetData.sourceAssetId, context, listener)
176+
}
177+
assetData.sourceUrl != null -> {
178+
downloadUrlAsset(assetData.sourceUrl, listener)
179+
}
180+
assetData.sourceAsset != null -> {
181+
loadBundledAsset(assetData.sourceAsset, assetData.path, context, listener)
182+
}
183+
}
184+
}
185+
186+
fun createCustomLoader(
187+
referencedAssets: ReferencedAssetsType?,
188+
cache: ReferencedAssetCache
189+
): FileAssetLoader? {
190+
val assetsData = referencedAssets?.data ?: return null
191+
val context = NitroModules.applicationContext ?: return null
192+
193+
return object : FileAssetLoader() {
194+
override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean {
195+
var key = asset.uniqueFilename.substringBeforeLast(".")
196+
var assetData = assetsData[key]
197+
198+
if (assetData == null) {
199+
key = asset.name
200+
assetData = assetsData[asset.name]
201+
}
202+
203+
if (assetData == null) {
204+
return false
205+
}
206+
207+
cache[key] = asset
208+
209+
loadAsset(assetData, asset, context)
210+
211+
return true
212+
}
213+
}
214+
}
215+
216+
fun dispose() {
217+
scope.cancel()
218+
}
219+
}

0 commit comments

Comments
 (0)