@@ -14,16 +14,102 @@ import java.io.File as JavaFile
1414import java.io.IOException
1515import java.net.URI
1616import java.net.URL
17+ import java.security.MessageDigest
18+ import java.util.concurrent.locks.ReentrantReadWriteLock
19+ import kotlin.concurrent.read
20+ import kotlin.concurrent.write
1721
1822typealias ReferencedAssetCache = MutableMap <String , FileAsset >
1923
2024class ReferencedAssetLoader {
2125 private val scope = CoroutineScope (Dispatchers .IO + SupervisorJob ())
26+ private var isDisposed = false
27+ private val cacheLock = ReentrantReadWriteLock ()
2228
2329 private fun logError (message : String ) {
2430 Log .e(" ReferencedAssetLoader" , message)
2531 }
2632
33+ private fun logDebug (message : String ) {
34+ Log .d(" ReferencedAssetLoader" , message)
35+ }
36+
37+ /* *
38+ * Get the cache directory for storing CDN assets
39+ */
40+ private fun getCacheDir (context : Context ): JavaFile {
41+ val cacheDir = context.cacheDir
42+ val riveCacheDir = JavaFile (cacheDir, " rive_assets" )
43+ if (! riveCacheDir.exists()) {
44+ riveCacheDir.mkdirs()
45+ }
46+ return riveCacheDir
47+ }
48+
49+ /* *
50+ * Generate a cache key from a URL or UUID
51+ */
52+ private fun generateCacheKey (urlOrUuid : String ): String {
53+ return try {
54+ val md = MessageDigest .getInstance(" MD5" )
55+ val hashBytes = md.digest(urlOrUuid.toByteArray())
56+ hashBytes.joinToString(" " ) { " %02x" .format(it) }
57+ } catch (e: Exception ) {
58+ // Fallback to hash code if MD5 is not available
59+ urlOrUuid.hashCode().toString().replace(" -" , " " )
60+ }
61+ }
62+
63+ /* *
64+ * Get cached file path for a URL/UUID
65+ */
66+ private fun getCachedFilePath (context : Context , urlOrUuid : String ): JavaFile {
67+ val cacheKey = generateCacheKey(urlOrUuid)
68+ return JavaFile (getCacheDir(context), cacheKey)
69+ }
70+
71+ /* *
72+ * Check if a cached file exists and is valid
73+ */
74+ private fun getCachedAsset (context : Context , urlOrUuid : String ): ByteArray? {
75+ return cacheLock.read {
76+ val cacheFile = getCachedFilePath(context, urlOrUuid)
77+ if (! cacheFile.exists()) {
78+ logDebug(" Cache miss for: $urlOrUuid " )
79+ return @read null
80+ }
81+
82+ try {
83+ val data = cacheFile.readBytes()
84+ if (data.isNotEmpty()) {
85+ logDebug(" Cache hit for: $urlOrUuid " )
86+ return @read data
87+ }
88+ } catch (e: Exception ) {
89+ logDebug(" Error reading cache for $urlOrUuid : ${e.message} " )
90+ }
91+
92+ null
93+ }
94+ }
95+
96+ /* *
97+ * Save asset data to cache
98+ */
99+ private fun saveToCache (context : Context , urlOrUuid : String , data : ByteArray ) {
100+ scope.launch(Dispatchers .IO ) {
101+ cacheLock.write {
102+ val cacheFile = getCachedFilePath(context, urlOrUuid)
103+ try {
104+ cacheFile.writeBytes(data)
105+ logDebug(" Saved to cache: $urlOrUuid (${data.size} bytes)" )
106+ } catch (e: Exception ) {
107+ logDebug(" Error saving cache for $urlOrUuid : ${e.message} " )
108+ }
109+ }
110+ }
111+ }
112+
27113 private fun isValidUrl (url : String ): Boolean {
28114 return try {
29115 URL (url)
@@ -63,7 +149,14 @@ class ReferencedAssetLoader {
63149 }
64150 }
65151
66- private fun downloadUrlAsset (url : String , listener : (ByteArray? ) -> Unit ) {
152+ private fun downloadUrlAsset (url : String , context : Context , listener : (ByteArray? ) -> Unit ) {
153+ // Check if disposed before starting download
154+ if (isDisposed) {
155+ logDebug(" Loader is disposed, skipping download: $url " )
156+ listener(null )
157+ return
158+ }
159+
67160 if (! isValidUrl(url)) {
68161 logError(" Invalid URL: $url " )
69162 listener(null )
@@ -82,10 +175,51 @@ class ReferencedAssetLoader {
82175 if (! file.canRead()) {
83176 throw IOException (" Permission denied: ${uri.path} " )
84177 }
85- file.readBytes()
178+ val fileBytes = file.readBytes()
179+ // Check again before calling listener
180+ if (isDisposed) {
181+ logDebug(" Loader disposed before calling listener for file: $url " )
182+ withContext(Dispatchers .Main ) {
183+ listener(null )
184+ }
185+ return @launch
186+ }
187+ fileBytes
86188 }
87189 " http" , " https" -> {
88- URL (url).readBytes()
190+ // Check cache first for HTTP/HTTPS URLs
191+ val cachedData = getCachedAsset(context, url)
192+ if (cachedData != null ) {
193+ // Check again before calling listener
194+ if (isDisposed) {
195+ logDebug(" Loader disposed before calling listener for cached: $url " )
196+ withContext(Dispatchers .Main ) {
197+ listener(null )
198+ }
199+ return @launch
200+ }
201+ withContext(Dispatchers .Main ) {
202+ listener(cachedData)
203+ }
204+ return @launch
205+ }
206+
207+ // Download from network
208+ val downloadedBytes = URL (url).readBytes()
209+
210+ // Save to cache
211+ saveToCache(context, url, downloadedBytes)
212+
213+ // Final check before calling listener
214+ if (isDisposed) {
215+ logDebug(" Loader disposed before calling listener: $url " )
216+ withContext(Dispatchers .Main ) {
217+ listener(null )
218+ }
219+ return @launch
220+ }
221+
222+ downloadedBytes
89223 }
90224 else -> {
91225 logError(" Unsupported URL scheme: ${uri.scheme} " )
@@ -118,7 +252,7 @@ class ReferencedAssetLoader {
118252 val scheme = runCatching { Uri .parse(sourceAssetId).scheme }.getOrNull()
119253
120254 if (scheme != null ) {
121- downloadUrlAsset(sourceAssetId, listener)
255+ downloadUrlAsset(sourceAssetId, context, listener)
122256 return @launch
123257 }
124258
@@ -180,6 +314,16 @@ class ReferencedAssetLoader {
180314 }
181315
182316 private fun processAssetBytes (bytes : ByteArray , asset : FileAsset ) {
317+ // Check if disposed before processing
318+ if (isDisposed) {
319+ logDebug(" Loader is disposed, skipping asset processing: ${asset.name} " )
320+ return
321+ }
322+
323+ if (bytes.isEmpty()) {
324+ return
325+ }
326+
183327 when (asset) {
184328 is ImageAsset -> asset.image = RiveRenderImage .make(bytes)
185329 is FontAsset -> asset.font = RiveFont .make(bytes)
@@ -188,9 +332,15 @@ class ReferencedAssetLoader {
188332 }
189333
190334 private fun loadAsset (assetData : ResolvedReferencedAsset , asset : FileAsset , context : Context ): Deferred <Unit > {
335+ // Check if disposed before starting
336+ if (isDisposed) {
337+ logDebug(" Loader is disposed, skipping asset load: ${asset.name} " )
338+ return CompletableDeferred <Unit >().apply { complete(Unit ) }
339+ }
340+
191341 val deferred = CompletableDeferred <Unit >()
192342 val listener: (ByteArray? ) -> Unit = { bytes ->
193- if (bytes != null ) {
343+ if (bytes != null && ! isDisposed ) {
194344 processAssetBytes(bytes, asset)
195345 }
196346 deferred.complete(Unit )
@@ -201,7 +351,7 @@ class ReferencedAssetLoader {
201351 loadResourceAsset(assetData.sourceAssetId, context, listener)
202352 }
203353 assetData.sourceUrl != null -> {
204- downloadUrlAsset(assetData.sourceUrl, listener)
354+ downloadUrlAsset(assetData.sourceUrl, context, listener)
205355 }
206356 assetData.sourceAsset != null -> {
207357 loadBundledAsset(assetData.sourceAsset, assetData.path, context, listener)
@@ -227,8 +377,49 @@ class ReferencedAssetLoader {
227377
228378 return object : FileAssetLoader () {
229379 override fun loadContents (asset : FileAsset , inBandBytes : ByteArray ): Boolean {
380+ // Check if disposed
381+ if (isDisposed) {
382+ logDebug(" Loader is disposed, skipping loadContents for: ${asset.name} " )
383+ return false
384+ }
385+
386+ // Check for CDN URL/UUID first (only if both are non-empty)
387+ val cdnUrl = asset.cdnUrl
388+
389+ if (cdnUrl != null && cdnUrl.isNotEmpty()) {
390+ logDebug(" Loading CDN asset from URL: $cdnUrl " )
391+
392+ val cached = getCachedAsset(context, cdnUrl)
393+ if (cached != null ) {
394+ // Use cached version
395+ scope.launch(Dispatchers .IO ) {
396+ if (! isDisposed) {
397+ processAssetBytes(cached, asset)
398+ }
399+ }
400+ cache[asset.uniqueFilename.substringBeforeLast(" ." )] = asset
401+ cache[asset.name] = asset
402+ return true
403+ } else {
404+ // Download and cache
405+ cache[asset.uniqueFilename.substringBeforeLast(" ." )] = asset
406+ cache[asset.name] = asset
407+
408+ val cdnAssetData = ResolvedReferencedAsset (
409+ sourceUrl = cdnUrl,
410+ sourceAssetId = null ,
411+ sourceAsset = null ,
412+ path = null
413+ )
414+ loadAsset(cdnAssetData, asset, context)
415+ return true
416+ }
417+ }
418+
230419 var key = asset.uniqueFilename.substringBeforeLast(" ." )
231420 var assetData = assetsData[key]
421+ cache[key] = asset
422+ cache[asset.name] = asset
232423
233424 if (assetData == null ) {
234425 key = asset.name
@@ -239,8 +430,6 @@ class ReferencedAssetLoader {
239430 return false
240431 }
241432
242- cache[key] = asset
243-
244433 loadAsset(assetData, asset, context)
245434
246435 return true
@@ -249,6 +438,7 @@ class ReferencedAssetLoader {
249438 }
250439
251440 fun dispose () {
441+ isDisposed = true
252442 scope.cancel()
253443 }
254444}
0 commit comments