Files
utun2/tools/chatgui-android/tests/test_core_activity.kt
T

121 lines
5.5 KiB
Kotlin

/* app_process + APK: настоящие CoreActivity/JNI/standby на Android main, без Activity и профиля приложения.
* Аргумент — новый /data/local/tmp/utun-activity-audit-<id>/profile; внешние подключения/обнаружение отключены.
* Native init/stop вызываются напрямую: frontend-владельцы камеры/аудио в этом процессе не создаются. */
import android.os.Handler
import android.os.Looper
import com.utun.chat.data.ChatEventCallback
import com.utun.chat.data.CoreActivity
import com.utun.chat.data.NativeLib
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlin.system.exitProcess
class ActivityProbeLog {
val commands = AtomicInteger()
val applied = Channel<Boolean>(Channel.UNLIMITED)
fun onNativeLog(level: Int, category: String, message: String) {
val prefix = "Android activity applied requested="
if (level != 1 || category != "sys" || !message.contains(prefix)) return
val value = message.substringAfter(prefix).take(1).toInt()
check(value in 0..1)
val effective = message.substringAfter("effective=").trim().toInt()
check(effective == value) { "activity mode mismatch requested=$value effective=$effective" }
commands.incrementAndGet()
check(applied.trySend(value != 0).isSuccess)
}
}
private fun nativeInit(directory: File, logger: ActivityProbeLog) {
val method = NativeLib::class.java.getDeclaredMethod("nativeInit", String::class.java, Int::class.javaPrimitiveType,
Any::class.java, Any::class.java)
method.isAccessible = true
check(method.invoke(NativeLib, directory.path, 0, null, logger) == true)
}
private fun nativeStop(name: String = "nativeStop"): Boolean {
val method = NativeLib::class.java.getDeclaredMethod(name)
method.isAccessible = true
return method.invoke(NativeLib) == true
}
fun main(args: Array<String>) {
check(args.size == 1)
val directory = File(args.single()).canonicalFile
check(directory.path.startsWith("/data/local/tmp/utun-activity-audit-") && !directory.exists())
check(directory.mkdirs())
if (Looper.getMainLooper() == null) Looper.prepareMainLooper()
val handler = Handler(Looper.getMainLooper())
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val ready = Channel<Unit>(Channel.UNLIMITED)
val logger = ActivityProbeLog()
NativeLib.setEventCallback(object : ChatEventCallback {
override fun onEvent(type: Int, data: ByteArray?) {
if (type == 12) handler.post {
CoreActivity.onCoreStarted()
check(ready.trySend(Unit).isSuccess)
}
}
})
suspend fun applied(expected: Boolean) = withTimeout(5_000) { check(logger.applied.receive() == expected) }
suspend fun drained() = withTimeout(5_000) {
NativeLib.ping()
while (!NativeLib.isResponsive()) delay(1)
check(logger.applied.tryReceive().isFailure) { "duplicate activity command" }
}
scope.launch {
try {
val config = "[global]\ndb_path=${directory.path}\nauto_sockets=no\n" +
"[ntp]\nenabled=no\n[stun]\nenabled=no\n[chatserver]\nstorage_autoload=0\n" +
"local_invites_enabled=0\nlocal_broadcasts_enabled=0\n[debug]\nsys=debug\n"
nativeInit(directory, logger)
check(NativeLib.start(config))
withTimeout(10_000) { ready.receive() }
applied(false)
CoreActivity.visible(true); applied(true)
CoreActivity.transfer(true)
CoreActivity.call(true)
CoreActivity.visible(false)
CoreActivity.call(false)
CoreActivity.transfer(false); applied(false)
repeat(3) { CoreActivity.sync() }
drained()
check(logger.commands.get() == 3)
CoreActivity.transfer(true); applied(true)
check(withContext(Dispatchers.IO) { nativeStop() })
check(!NativeLib.isReady() && !NativeLib.setActive(true))
CoreActivity.sync()
nativeInit(directory, logger)
check(NativeLib.start(config))
withTimeout(10_000) { ready.receive() }
applied(true)
CoreActivity.transfer(false); applied(false)
val rejected = AtomicReference<Throwable?>()
val background = Thread({
try { CoreActivity.call(true) } catch (error: Throwable) { rejected.set(error) }
}, "activity-invalid-caller")
background.start(); background.join(2_000)
check(!background.isAlive && rejected.get() is IllegalStateException)
CoreActivity.sync()
drained()
check(logger.commands.get() == 6)
println("PASS: Android activity owners, accepted JNI queue, native ACTIVE/STANDBY, restart resync and thread guard")
} catch (error: Throwable) {
error.printStackTrace(System.err)
throw error
} finally { check(withContext(Dispatchers.IO) { nativeStop("nativeDestroy") }) }
exitProcess(0)
}
Looper.loop()
}