diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt index d0f37657..3b3a4241 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt +++ b/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) + val command: Int, val result: CompletableDeferred, 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() - 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") } - } + } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 3e708eb0..78418ad4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/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) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt index dccfc5a2..9c8ef612 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt +++ b/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) } }) }