Browse Source

android: cancel avatar operations before native teardown and bound image preparation

master
evgeny 6 days ago
parent
commit
9c70eab075
  1. 18
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt
  2. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  3. 1
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt

18
tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt

@ -27,6 +27,7 @@ class AvatarRepository(context: Context) {
private val app = context.applicationContext
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val transfers = Semaphore(2)
private val preparations = Semaphore(2)
private val ids = AtomicLong(System.nanoTime())
private var generation = 0L
private var account = 0L
@ -73,7 +74,7 @@ class AvatarRepository(context: Context) {
}
private data class Pending(val type: Int, val group: Long, val server: Long, val owner: Long,
val command: Int, val result: CompletableDeferred<ByteArray>)
val command: Int, val result: CompletableDeferred<ByteArray>, var cancelled: Boolean = false)
private fun log(level: String, message: String) = LogManager.addLog(level, "Avatar", message)
private fun changed() { _revision.value++ }
private fun alive(epoch: Long) { if (generation != epoch || account == 0L) throw CancellationException("avatar session changed") }
@ -157,11 +158,15 @@ class AvatarRepository(context: Context) {
private fun reset() {
log("INFO", "Reset account=${AvatarProtocol.hex(account)} pending=${pending.size} cacheBytes=${cache.size()}")
generation++
pending.forEach { (id, p) ->
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()
account = 0; defaultAvatar = null; cache.evictAll()
_members.value = emptyMap(); _groups.value = emptyMap(); _edits.value = emptyMap(); changed()
}
fun stop() { reset() }
private fun start() {
val uid = NativeLib.getMyNodeId()
if (uid == 0L || uid == account) return
@ -188,7 +193,8 @@ class AvatarRepository(context: Context) {
enqueue: (Long, Long) -> Boolean): ByteArray {
val id = ids.incrementAndGet(); val gid = AvatarProtocol.node(group)
val result = CompletableDeferred<ByteArray>()
pending[id] = Pending(type, gid, server, owner, command, result)
val operation = Pending(type, gid, server, owner, command, result)
pending[id] = operation
var complete = false
try {
if (!enqueue(id, gid)) throw IOException("native enqueue failed id=$id type=$type group=$group")
@ -197,7 +203,7 @@ class AvatarRepository(context: Context) {
return data
} finally {
pending.remove(id)
if (!complete && type in 49..50 && NativeLib.isReady()) NativeLib.cancelAvatar(id)
if (!complete && !operation.cancelled && type in 49..50 && NativeLib.isReady()) NativeLib.cancelAvatar(id)
}
}
@ -431,7 +437,7 @@ class AvatarRepository(context: Context) {
}
/** Чтение URI, EXIF, crop и кодирование выполняются вне UI и потока ядра. */
private suspend fun prepare(uri: Uri, group: Boolean): ByteArray = withContext(Dispatchers.IO) {
private suspend fun prepare(uri: Uri, group: Boolean): ByteArray = preparations.withPermit { withContext(Dispatchers.IO) {
val temp = File.createTempFile("avatar-source-", ".image", app.cacheDir)
try {
app.contentResolver.openInputStream(uri)?.use { input ->
@ -445,7 +451,7 @@ class AvatarRepository(context: Context) {
}
} ?: throw IOException("cannot read selected image")
val (w, h) = PhotoUtil.orientedDimensions(temp.path)
require(w > 0 && h > 0 && w.toLong() * h <= 100_000_000) { "invalid source dimensions $w x $h" }
require(w > 0 && h > 0 && w.toLong() * h <= 16L * 1024 * 1024) { "invalid source dimensions $w x $h" }
var sample = 1
while (maxOf(w, h) / sample > 1024) sample *= 2
val decoded = PhotoUtil.decodeOriented(temp.path, sample) ?: throw IOException("source decode failed")
@ -468,5 +474,5 @@ class AvatarRepository(context: Context) {
decoded.recycle()
}
} finally { if (!temp.delete()) log("WARN", "Cannot delete source temp=$temp") }
}
} }
}

6
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -42,7 +42,7 @@ object NativeLib {
}
fun stop() {
runBlocking(Dispatchers.Main.immediate) { CallController.reset() }
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
if (!isReady()) return
nativeStop()
}
@ -54,12 +54,12 @@ object NativeLib {
}
fun destroy() {
runBlocking(Dispatchers.Main.immediate) { CallController.reset() }
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
nativeDestroy()
}
fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
runBlocking(Dispatchers.Main.immediate) { CallController.reset() }
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
return nativeRestart(configText)
}

1
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt

@ -62,6 +62,7 @@ fun GroupPropertiesDialog(channel: Channel, onDismiss: () -> Unit) {
Text(stringResource(R.string.sound), modifier = Modifier.weight(1f))
Switch(checked = soundOn, onCheckedChange = {
soundOn = it; NotificationSound.setMuted(channel.id, !it)
NativeLib.setSoundOnMessage(channel.id, it)
scope.launch { ChatApplication.instance.configProvider.setChannelMuted(channel.id, !it) }
})
}

Loading…
Cancel
Save