diff --git a/tests/test_android_replies.c b/tests/test_android_replies.c index 553332772..c098ad4fd 100644 --- a/tests/test_android_replies.c +++ b/tests/test_android_replies.c @@ -8,6 +8,8 @@ #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_event.h" +#include "../src/dm/dm_core.h" +#include "../src/transport_layer/secure_channel.h" #include "../src/media_async/attachment_send.h" #include "../src/media_async/encrypted_file.h" #include "../src/routing_layer/topo_node_sqlite.h" @@ -191,6 +193,47 @@ static int check_local_sockets(void) { u_free(json); return 0; } +/* Android PM ждёт commit, а отказ очереди не создаёт ожидание события; текст сохраняет обычный UTF-8. */ +static int check_dm_submissions(void) { + struct SC_MYKEYS peer; uint8_t ed[32]; + REQUIRE(!sc_generate_keypair(&peer) && !sc_derive_ed25519_pubkey(peer.private_key, ed)); + uint64_t node = sc_derive_node_id_from_pubkey(peer.public_key); + sqlite3_stmt* st = NULL; + REQUIRE(sqlite3_prepare_v2(instance->topo_sqlite_db, + "INSERT INTO nodes(node_id,x25519_pubkey,ed25519_pubkey,name) VALUES(?,?,?,'Android PM peer')", -1, &st, NULL) == SQLITE_OK); + sqlite3_bind_int64(st, 1, (sqlite3_int64)node); + sqlite3_bind_blob(st, 2, peer.public_key, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 3, ed, 32, SQLITE_STATIC); + int rc = sqlite3_step(st); sqlite3_finalize(st); REQUIRE(rc == SQLITE_DONE); + REQUIRE(!dm_start(instance, node, peer.public_key, ed, "Android PM peer", NULL)); + char conv[64], before[8192], after[8192], value[256]; size_t length = 0; + snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(instance->node_id, node)); + uint64_t generation = dm_history_generation(instance, conv); REQUIRE(generation); + const uint8_t text[] = "Привет 😀 \"PM\"\nследующая строка"; + REQUIRE(!utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 11) && wait_result(11) == 1); + REQUIRE(!dm_list_messages_json(instance, conv, 20, 0, before, sizeof(before), &length)); + REQUIRE(!extract(before, "$[0].data", value, sizeof(value)) && !strcmp(value, (const char*)text)); + REQUIRE(!utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation + 1, 12) && wait_result(12) == -1); + REQUIRE(sqlite3_exec(instance->topo_sqlite_db, "CREATE TEMP TRIGGER android_dm_write_fail BEFORE INSERT ON dm_outbox" + " BEGIN SELECT RAISE(FAIL,'android fixture write failure'); END", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(!utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 13) && wait_result(13) == -1); + REQUIRE(sqlite3_exec(instance->topo_sqlite_db, "DROP TRIGGER android_dm_write_fail", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(!dm_list_messages_json(instance, conv, 20, 0, after, sizeof(after), &length) && !strcmp(before, after)); + REQUIRE(!utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 14) && wait_result(14) == 1); + const uint8_t embedded_nul[] = {'a', 0, 'b'}; + REQUIRE(utun_bridge_dm_send(conv, embedded_nul, sizeof(embedded_nul), generation, 15) == -1); + REQUIRE(utun_bridge_dm_send(conv, text, 0, generation, 15) == -1); + REQUIRE(utun_bridge_dm_send(conv, text, sizeof(text) - 1, 0, 15) == -1); + REQUIRE(utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 0) == -1); + uint8_t oversized[DM_DATA_MAX + 1] = {0}; + REQUIRE(utun_bridge_dm_send(conv, oversized, sizeof(oversized), generation, 16) == -1); + struct UTUN_INSTANCE* saved = instance; instance = NULL; + rc = utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 17); instance = saved; + REQUIRE(rc == -1); + uasync_poll(async, 0); REQUIRE(!results[15] && !results[16] && !results[17]); + printf("[PASS] Android PM: UTF-8, commit ACK, stale generation, SQL rollback/retry and rejected requests\n"); + return 0; +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); char directory[512] = "/tmp/utun_android_replies_XXXXXX", config[1024], channel[64], hex[65]; @@ -206,6 +249,7 @@ int main(void) { REQUIRE(!extract(json, "$[0].id", channel, sizeof(channel))); u_free(json); REQUIRE(!check_addresses(channel)); REQUIRE(!check_local_sockets()); + REQUIRE(!check_dm_submissions()); uint8_t empty[32] = {0}, original[32], missing[32]; memset(missing, 0xff, sizeof(missing)); REQUIRE(!utun_bridge_send_message(channel, "Привет 😀 \"original\"\nnext line", empty, 1) && wait_result(1) == 1); json = utun_bridge_get_messages_json(channel, 1, NULL, 0); diff --git a/tools/chatgui-android/doc/ARCHITECTURE.md b/tools/chatgui-android/doc/ARCHITECTURE.md index 2682c306b..34a520ff1 100644 --- a/tools/chatgui-android/doc/ARCHITECTURE.md +++ b/tools/chatgui-android/doc/ARCHITECTURE.md @@ -245,6 +245,9 @@ ConversationComposer сохраняет draft, URI и тип вложения ч показывается ресурсом интерфейса; ошибка cleanup логируется отдельно. SAF export в канале и PM сообщает об отказе запуска picker/записи. Null output stream не считается успешным сохранением; ожидающий путь переживает пересоздание Activity. +PM text передаётся JNI как обычные UTF-8 bytes, generation и ненулевой request ID. +Успешный enqueue не очищает draft: composer ждёт MSG_SUBMITTED после транзакции истории/outbox. +При stale generation/SQL error draft остаётся, отказ очереди завершает ожидание сразу. Звонок и рация используют общий C голосовой стек. Kotlin владеет AudioRecord/AudioTrack, feed/pull workers и маршрутом Android Telecom; DSP и сетевой сигналинг разделены. @@ -283,6 +286,11 @@ target. Detach/close продолжают освобождение незави через 250 мс..30 с. При отказе Handler повторный release завершает её явно. `CallAudioRouter` сохраняет device callback и каждый полученный Bluetooth proxy до +успешного unregister/close. ProximityManager также сохраняет прежний sensor listener +и его manager при отказе unregister: start ждёт cleanup, Handler повторяет его через +250 мс..30 с, повторный stop работает после отказа таймера. Старая регистрация и +повреждённые показания не дают ложного FAR; после lookup failure start повторяет поиск. +CallAudioRouter ждёт успешного unregister/close. stop инвалидирует callbacks и снимки устройств сразу, очищает независимые ресурсы и бросает ошибку при неподтверждённом результате. Собственные communication device/SCO-заявки отмечаются до API и снимаются отдельно;