Load Android gallery objects and catalogs through the shared cache
This commit is contained in:
1 parent
f27419165d
commit
ea6b895df9
1 file changed
+76
-34
@@ -44,6 +44,7 @@ class AvatarRepository(context: Context) {
|
||||
private val _edits = MutableStateFlow<Map<String, AvatarEditState>>(emptyMap())
|
||||
val edits: StateFlow<Map<String, AvatarEditState>> = _edits
|
||||
private val loads = mutableSetOf<String>()
|
||||
private val objectLoads = mutableMapOf<String, Deferred<Unit>>()
|
||||
private val memberRequests = mutableMapOf<String, Long>()
|
||||
private val editTokens = mutableMapOf<String, Long>()
|
||||
private val replications = mutableSetOf<String>()
|
||||
@@ -64,7 +65,7 @@ class AvatarRepository(context: Context) {
|
||||
scope.launch {
|
||||
try {
|
||||
val bitmap = withContext(Dispatchers.IO) {
|
||||
decode(readImage(File(directory(uid), file)), file.removeSuffix(".png"), false)
|
||||
val root=directory(uid); AvatarAssets.image(root,AvatarAssets.record(root,file),false)
|
||||
}
|
||||
alive(epoch); cache.put(file, bitmap); changed()
|
||||
} catch (e: CancellationException) { throw e }
|
||||
@@ -162,7 +163,7 @@ class AvatarRepository(context: Context) {
|
||||
if (p.type in 49..50 && NativeLib.isReady()) { p.cancelled = true; NativeLib.cancelAvatar(id) }
|
||||
}
|
||||
scope.coroutineContext.cancelChildren()
|
||||
pending.clear(); loads.clear(); replications.clear(); dirtyGroups.clear(); memberRequests.clear(); editTokens.clear()
|
||||
pending.clear(); loads.clear(); objectLoads.clear(); replications.clear(); dirtyGroups.clear(); memberRequests.clear(); editTokens.clear()
|
||||
account = 0; defaultAvatar = null; cache.evictAll()
|
||||
_members.value = emptyMap(); _groups.value = emptyMap(); _edits.value = emptyMap(); changed()
|
||||
}
|
||||
@@ -274,24 +275,7 @@ class AvatarRepository(context: Context) {
|
||||
if (candidates.isEmpty()) break
|
||||
val source = candidates[retry % candidates.size]
|
||||
try {
|
||||
val bitmap = transfers.withPermit {
|
||||
val root = directory(uid); val file = File(root, key)
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!root.isDirectory && !root.mkdirs()) throw IOException("cannot create cache directory")
|
||||
if (file.isFile) {
|
||||
try {
|
||||
decode(readImage(file), key.removeSuffix(".png"), false).also {
|
||||
if (!file.setLastModified(System.currentTimeMillis())) log("WARN", "Cannot touch cache file=$file")
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log("WARN", "Invalid cached avatar file=$key: ${e.message}")
|
||||
if (!file.delete()) log("WARN", "Cannot delete corrupt cache file=$file")
|
||||
null
|
||||
}
|
||||
} else null
|
||||
} ?: download(source.key.group, source.value, root)
|
||||
}
|
||||
val bitmap=loadAvatar(source.key.group,source.value,false)
|
||||
alive(epoch); cache.put(key, bitmap); changed()
|
||||
log("INFO", "User image ready group=$sourceGroup owner=${AvatarProtocol.hex(owner)} file=$key retry=$retry")
|
||||
break
|
||||
@@ -311,22 +295,80 @@ class AvatarRepository(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun download(group: String, m: ChatMember, root: File): Bitmap {
|
||||
private suspend fun objectFile(group: String, m: ChatMember, file: String, limit: Int) {
|
||||
require(_members.value[AvatarMemberKey(group, m.avatarServer)]?.storageRole == true) { "avatar server has no signed storage role" }
|
||||
val temp = File(root, ".download-${ids.incrementAndGet()}")
|
||||
try {
|
||||
val reply = AvatarProtocol.transfer(request(50, group, m.avatarServer, m.nodeId) { id, gid ->
|
||||
NativeLib.transferAvatar(id, gid, m.avatarServer, m.nodeId, "avatars/${m.avatarFile}", temp.path)
|
||||
}) ?: throw IOException("missing final transfer result")
|
||||
require(reply.status == 0 && reply.size in 1..AvatarProtocol.MAX_USER) { "download status=${reply.status} size=${reply.size}" }
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = decode(readImage(temp), m.avatarFile.removeSuffix(".png"), false)
|
||||
val target = File(root, m.avatarFile)
|
||||
if (!temp.renameTo(target)) throw IOException("cannot publish validated cache file")
|
||||
prune(root)
|
||||
bitmap
|
||||
val epoch=generation; val root=directory(account)
|
||||
val operation=objectLoads[file] ?: scope.async {
|
||||
transfers.withPermit {
|
||||
val target=File(root,file)
|
||||
val hit=withContext(Dispatchers.IO) {
|
||||
if (!target.isFile) false else try { AvatarAssets.read(target,file,limit); true }
|
||||
catch (e: Exception) {
|
||||
log("WARN", "Invalid cache object=$file: ${e.message}")
|
||||
if (!target.delete()) log("WARN", "Cannot delete invalid cache=$target")
|
||||
false
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
withContext(Dispatchers.IO) { if (!root.isDirectory && !root.mkdirs()) throw IOException("cannot create avatar cache") }
|
||||
val temp=File(root,".download-${ids.incrementAndGet()}")
|
||||
try {
|
||||
val reply=AvatarProtocol.transfer(request(50,group,m.avatarServer,m.nodeId) { id,gid ->
|
||||
NativeLib.transferAvatar(id,gid,m.avatarServer,m.nodeId,"avatars/$file",temp.path,maxBytes=limit)
|
||||
}) ?: throw IOException("missing transfer result")
|
||||
require(reply.status==0 && reply.size in 1..limit) { "download status=${reply.status} size=${reply.size}" }
|
||||
withContext(Dispatchers.IO) { AvatarAssets.store(root,file,AvatarAssets.read(temp,file,limit)) }
|
||||
} finally { withContext(NonCancellable+Dispatchers.IO) {
|
||||
if (temp.exists() && !temp.delete()) log("WARN", "Cannot delete download temp=$temp")
|
||||
} }
|
||||
}
|
||||
alive(epoch); log("DEBUG", "Object ready file=$file cache=$hit group=$group")
|
||||
}
|
||||
} finally { withContext(NonCancellable + Dispatchers.IO) { if (temp.exists() && !temp.delete()) log("WARN", "Cannot delete temp=$temp") } }
|
||||
}.also { objectLoads[file]=it }
|
||||
try { operation.await(); alive(epoch) }
|
||||
finally { if (generation==epoch && operation.isCompleted && objectLoads[file]===operation) objectLoads.remove(file) }
|
||||
}
|
||||
|
||||
suspend fun loadAvatar(group: String, m: ChatMember, original: Boolean): Bitmap {
|
||||
val epoch=generation; val root=directory(account)
|
||||
if (!original) cache.get(m.avatarFile)?.let { return it }
|
||||
objectFile(group,m,m.avatarFile,AvatarProtocol.MAX_MANIFEST)
|
||||
val record=withContext(Dispatchers.IO) { AvatarAssets.record(root,m.avatarFile) }
|
||||
objectFile(group,m,if (original) record.original else record.preview,
|
||||
if (original) AvatarProtocol.MAX_ORIGINAL else AvatarProtocol.MAX_USER)
|
||||
val bitmap=withContext(Dispatchers.IO) { AvatarAssets.image(root,record,original).also { prune(root) } }
|
||||
alive(epoch); if (!original) { cache.put(m.avatarFile,bitmap); changed() }
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun gallerySource(group: String?, owner: Long): Pair<String,ChatMember>? {
|
||||
val m=member(group,owner) ?: return null
|
||||
if (m.avatarServer==0L || !AvatarProtocol.validFile(m.avatarFile)) return null
|
||||
val source=group ?: _members.value.entries.firstOrNull { it.value==m }?.key?.group ?: return null
|
||||
return source to m
|
||||
}
|
||||
|
||||
suspend fun catalog(group: String, m: ChatMember): List<String> {
|
||||
val epoch=generation; var restarts=0
|
||||
while (true) {
|
||||
var cursor=0L; var version=ByteArray(16); val entries=linkedMapOf<String,Long>(); var restart=false
|
||||
do {
|
||||
val reply=AvatarProtocol.response(request(49,group,m.avatarServer,m.nodeId,1) { id,gid ->
|
||||
NativeLib.avatarList(id,gid,m.avatarServer,m.nodeId,cursor,version)
|
||||
})
|
||||
alive(epoch); require(reply.path=="avatars") { "catalog path mismatch" }
|
||||
if (reply.status in listOf(6,7) && restarts++<3) { restart=true; delay(1000); break }
|
||||
require(reply.status==0) { "catalog status=${reply.status}" }
|
||||
AvatarProtocol.entries(reply).forEach { entries[it.file]=it.mtime }
|
||||
if (reply.more) require(reply.cursor>cursor) { "catalog cursor did not advance" }
|
||||
cursor=reply.cursor; version=reply.version
|
||||
} while (reply.more)
|
||||
if (restart) continue
|
||||
val files=entries.keys.sortedWith(compareByDescending<String> { entries[it] }.thenBy { it }).toMutableList()
|
||||
if (m.avatarFile !in files) files.add(0,m.avatarFile)
|
||||
log("INFO", "Catalog ready group=$group owner=${AvatarProtocol.hex(m.nodeId)} count=${files.size}")
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
private fun readImage(file: File): ByteArray {
|
||||
|
||||
Reference in new issue
Block a user