Browse Source

Expose member file commands through Android JNI

master
evgeny 6 days ago
parent
commit
e97885eb22
  1. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt
  2. 23
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  3. 86
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

4
tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt

@ -14,6 +14,7 @@ import com.utun.chat.data.TelecomCallManager
import com.utun.chat.data.ChatEventCallback
import com.utun.chat.data.CallController
import com.utun.chat.data.AvatarRepository
import com.utun.chat.data.MemberFilesRepository
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.CoroutineScope
@ -30,6 +31,7 @@ object AppEventHandler : ChatEventCallback {
val events: SharedFlow<Pair<Int, ByteArray?>> = _events
override fun onEvent(type: Int, data: ByteArray?) {
if (type == 13 || type in 49..50) mainHandler.post { ChatApplication.instance.memberFiles.onEvent(type, data) }
if (type in listOf(4, 8, 9, 12, 13, 23, 24, 25, 31) || type in 49..53)
mainHandler.post { ChatApplication.instance.avatars.onEvent(type, data) }
if (type == 1 || type == 32) playMessageSound(type, data)
@ -61,6 +63,7 @@ class ChatApplication : Application() {
lateinit var logManager: LogManager
lateinit var configProvider: ConfigProvider
lateinit var avatars: AvatarRepository
lateinit var memberFiles: MemberFilesRepository
override fun onCreate() {
super.onCreate()
@ -68,6 +71,7 @@ class ChatApplication : Application() {
logManager = LogManager
configProvider = ConfigProvider(this)
avatars = AvatarRepository(this)
memberFiles = MemberFilesRepository(this)
/* Notification sound state: global toggle + per-group mute */
NotificationSound.enabled = configProvider.getSoundEnabled()

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

@ -42,9 +42,12 @@ object NativeLib {
}
fun stop() {
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
runBlocking(Dispatchers.Main.immediate) {
CallController.reset(); ChatApplication.instance.avatars.stop(); ChatApplication.instance.memberFiles.cancel()
}
if (!isReady()) return
nativeStop()
runBlocking(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() }
}
fun setEventCallback(cb: ChatEventCallback) {
@ -54,12 +57,17 @@ object NativeLib {
}
fun destroy() {
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
runBlocking(Dispatchers.Main.immediate) {
CallController.reset(); ChatApplication.instance.avatars.stop(); ChatApplication.instance.memberFiles.cancel()
}
nativeDestroy()
runBlocking(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() }
}
fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
runBlocking(Dispatchers.Main.immediate) { CallController.reset(); ChatApplication.instance.avatars.stop() }
runBlocking(Dispatchers.Main.immediate) {
CallController.reset(); ChatApplication.instance.avatars.stop(); ChatApplication.instance.memberFiles.cancel()
}
return nativeRestart(configText)
}
@ -137,6 +145,15 @@ object NativeLib {
fun setGroupAvatar(id: Long, group: Long, data: ByteArray): Boolean =
nativeAvatarOperation(6, id, group, 0, 0, null, null, data)
fun cancelAvatar(id: Long): Boolean = nativeAvatarOperation(7, id, 0, 0, 0, null, null, null)
fun fileRequest(group: Long, server: Long, wire: ByteArray): Boolean = nativeFileRequest(group, server, wire)
fun fileTransfer(id: Long, group: Long, server: Long, owner: Long, remote: String, local: String,
upload: Boolean, version: ByteArray): Boolean =
nativeFileTransfer(id, group, server, owner, remote.toByteArray(), local.toByteArray(), upload, version)
fun fileCancel(id: Long): Boolean = nativeFileCancel(id)
private external fun nativeFileRequest(group: Long, server: Long, wire: ByteArray): Boolean
private external fun nativeFileTransfer(id: Long, group: Long, server: Long, owner: Long, remote: ByteArray,
local: ByteArray, upload: Boolean, version: ByteArray): Boolean
private external fun nativeFileCancel(id: Long): Boolean
fun getMemberDetail(channelId: String, nodeId: Long): String = nativeGetMemberDetail(channelId, nodeId) ?: "{}"
fun setMemberNick(channelId: String, nodeId: Long, nick: String) { nativeSetMemberNick(channelId, nodeId, nick) }
fun toggleMemberRadioMute(channelId: String, nodeId: Long): Boolean = nativeToggleMemberRadioMute(channelId, nodeId)

86
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -43,6 +43,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>
#include <limits.h>
#include <unistd.h>
@ -2961,4 +2962,89 @@ invalid:
u_free(req); return JNI_FALSE;
}
/* Общие файловые команды. Wire уже UTF-8, поэтому JNI modified UTF-8 не портит имена с emoji. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileRequest(
JNIEnv* env, jobject thiz, jlong group, jlong server, jbyteArray wire) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
jsize size = wire ? (*env)->GetArrayLength(env, wire) : 0;
struct mf_gui_request* q = NULL;
if (!inst || !ua || !uasync_is_running(ua) || !group || !server || size < MF_WIRE_HEADER || size > MF_WIRE_MAX) goto invalid;
uint8_t bytes[MF_WIRE_MAX];
(*env)->GetByteArrayRegion(env, wire, 0, size, (jbyte*)bytes);
if ((*env)->ExceptionCheck(env)) goto invalid;
q = u_calloc(1, sizeof(*q));
if (!q || mf_decode(bytes, size, &q->message)) goto invalid;
struct mf_message* m = &q->message;
int read = m->command == MF_LIST || m->command == MF_STAT || m->command == MF_QUOTA;
int write = m->command == MF_MKDIR || m->command == MF_RENAME || m->command == MF_REMOVE_PATH;
if (m->kind != MF_REQUEST || (!read && !write) || m->data_len || m->offset || m->size || m->more ||
!mf_path_valid(m->path, read) || (!m->owner && (m->command != MF_LIST || m->path[0])) ||
(m->command == MF_RENAME ? !mf_path_valid(m->target, 0) : m->target[0] != 0)) goto invalid;
if (write && (m->owner != inst->node_id ||
(!strncasecmp(m->path, "avatars", 7) && (!m->path[7] || m->path[7] == '/')))) goto invalid;
if (m->command == MF_RENAME && !strncasecmp(m->target, "avatars", 7) && (!m->target[7] || m->target[7] == '/')) goto invalid;
q->inst = inst; q->group = (uint64_t)group; q->server = (uint64_t)server;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "Android files: request id=%llu cmd=%s group=%llu server=%llu owner=%llu path=%s",
(unsigned long long)m->request_id, mf_command_text(m->command), (unsigned long long)group,
(unsigned long long)server, (unsigned long long)m->owner, m->path);
uasync_post(ua, member_files_gui_request, q); return JNI_TRUE;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: request rejected group=%llu server=%llu bytes=%d ready=%d",
(unsigned long long)group, (unsigned long long)server, (int)size, inst && ua && uasync_is_running(ua));
u_free(q); return JNI_FALSE;
}
/* Копирование настоящего UTF-8 пути с проверкой длины и отсутствия NUL. */
static int android_file_path(JNIEnv* env, jbyteArray bytes, char* out) {
jsize size = bytes ? (*env)->GetArrayLength(env, bytes) : 0;
if (size < 1 || size > MF_PATH_MAX) return -1;
(*env)->GetByteArrayRegion(env, bytes, 0, size, (jbyte*)out);
if ((*env)->ExceptionCheck(env) || memchr(out, 0, size)) return -1;
out[size] = 0; return 0;
}
/* Handle/локальный файл принадлежат общему transfer, Kotlin ждёт его терминальное событие. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileTransfer(
JNIEnv* env, jobject thiz, jlong id, jlong group, jlong server, jlong owner,
jbyteArray remote, jbyteArray local, jboolean upload, jbyteArray version) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
struct mf_transfer_request* q = NULL;
if (!inst || !ua || !uasync_is_running(ua) || !id || !group || !server || !owner ||
!version || (*env)->GetArrayLength(env, version) != 16) goto invalid;
q = u_calloc(1, sizeof(*q));
if (!q || android_file_path(env, remote, q->remote) || android_file_path(env, local, q->local) ||
!mf_path_valid(q->remote, 0) || q->local[0] != '/') goto invalid;
(*env)->GetByteArrayRegion(env, version, 0, 16, (jbyte*)q->version);
if ((*env)->ExceptionCheck(env) || (!upload && mf_zero(q->version, 16))) goto invalid;
if (upload && (owner != (jlong)inst->node_id ||
(!strncasecmp(q->remote, "avatars", 7) && (!q->remote[7] || q->remote[7] == '/')))) goto invalid;
q->inst = inst; q->id = id; q->group = group; q->server = server; q->owner = owner; q->upload = upload != JNI_FALSE;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: transfer queued id=%llu group=%llu server=%llu owner=%llu upload=%d path=%s",
(unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, q->upload, q->remote);
uasync_post(ua, member_files_gui_transfer, q); return JNI_TRUE;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: transfer rejected id=%llu group=%llu server=%llu owner=%llu upload=%d",
(unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, upload);
u_free(q); return JNI_FALSE;
}
/* Отмена по уникальному ID не затрагивает передачи других владельцев. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileCancel(JNIEnv* env, jobject thiz, jlong id) {
(void)env; (void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!id || !inst || !ua || !uasync_is_running(ua)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "Android files: cancel unavailable id=%llu", (unsigned long long)id); return JNI_FALSE;
}
struct mf_cancel_request* q = u_calloc(1, sizeof(*q));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: cancel allocation failed"); return JNI_FALSE; }
q->inst = inst; q->id = id;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: cancel queued id=%llu", (unsigned long long)id);
uasync_post(ua, member_files_gui_cancel, q); return JNI_TRUE;
}
#endif /* __ANDROID__ */

Loading…
Cancel
Save