Skip to content

Commit f68cf0c

Browse files
committed
feat(cdn/viewDispoal): adding in cdn caching and view disposal checking
1 parent fde1c81 commit f68cf0c

File tree

2 files changed

+433
-17
lines changed

2 files changed

+433
-17
lines changed

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

Lines changed: 198 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,102 @@ import java.io.File as JavaFile
1414
import java.io.IOException
1515
import java.net.URI
1616
import 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

1822
typealias ReferencedAssetCache = MutableMap<String, FileAsset>
1923

2024
class 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

Comments
 (0)