Expose member file commands through Android JNI
This commit is contained in:
1 parent
51e7f821e9
commit
e97885eb22
3 files changed
+110
-3
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__ */
|
||||
Reference in new issue
Block a user