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

117 lines
6.0 KiB
Kotlin

/* app_process + APK: настоящие PhoneStateMonitor.publish/NativeLib/JNI без Activity и профиля приложения.
* Новый /data/local/tmp/utun-phone-audit-<id>/profile; сеть/обнаружение отключены, состояние SIM задаёт тест.
* Приватный publisher вызывается отдельно от Android Telephony; frontend media-владельцы не создаются. */
import android.os.Handler
import android.os.Looper
import com.utun.chat.data.ChatEventCallback
import com.utun.chat.data.LogManager
import com.utun.chat.data.NativeLib
import com.utun.chat.data.PhoneStateMonitor
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
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 PhoneStateProbeLog {
val commands = AtomicInteger()
val applied = Channel<Pair<Int, Int>>(Channel.UNLIMITED)
fun onNativeLog(level: Int, category: String, message: String) {
val prefix = "Android phone state applied state="
if (level != 1 || category != "call" || !message.contains(prefix)) return
val state = message.substringAfter(prefix).substringBefore(' ').toInt()
val policy = message.substringAfter("policy=").trim().toInt()
commands.incrementAndGet()
check(applied.trySend(state to policy).isSuccess)
}
}
private fun nativeInit(directory: File, logger: PhoneStateProbeLog) {
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 native(name: String, config: String? = null): Boolean {
val method = if (config == null) NativeLib::class.java.getDeclaredMethod(name)
else NativeLib::class.java.getDeclaredMethod(name, String::class.java)
method.isAccessible = true
return (if (config == null) method.invoke(NativeLib) else method.invoke(NativeLib, config)) == true
}
private fun publish(state: Int, policy: Int) {
val field = PhoneStateMonitor::class.java.getDeclaredField("policy")
field.isAccessible = true; field.setInt(null, policy)
val method = PhoneStateMonitor::class.java.getDeclaredMethod("publish", Int::class.javaPrimitiveType)
method.isAccessible = true; method.invoke(PhoneStateMonitor, state)
}
fun main(args: Array<String>) {
check(args.size == 1)
val directory = File(args.single()).canonicalFile
check(directory.path.startsWith("/data/local/tmp/utun-phone-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 = PhoneStateProbeLog()
NativeLib.setEventCallback(object : ChatEventCallback {
override fun onEvent(type: Int, data: ByteArray?) { if (type == 12) handler.post { check(ready.trySend(Unit).isSuccess) } }
})
suspend fun applied(state: Int, policy: Int) = withTimeout(5_000) { check(logger.applied.receive() == state to policy) }
suspend fun drained() = withTimeout(5_000) {
NativeLib.ping()
while (!NativeLib.isResponsive()) delay(1)
check(logger.applied.tryReceive().isFailure) { "unexpected phone state command" }
}
scope.launch {
try {
val config = "[global]\ndb_path=${directory.path}\nauto_sockets=no\ndebug_level=error\n" +
"[ntp]\nenabled=no\n[stun]\nenabled=no\n[chatserver]\nstorage_autoload=0\n" +
"local_invites_enabled=0\nlocal_broadcasts_enabled=0\n[debug]\ncall=debug\n"
check(!NativeLib.setPhoneState(2, 1))
publish(2, 1) // Отложенный publish не должен подавлять тот же state после готовности ядра.
nativeInit(directory, logger)
check(NativeLib.start(config))
withTimeout(10_000) { ready.receive() }
publish(2, 1); applied(2, 1)
publish(2, 32) // Инъекция отказа без OOM: неверная маска отклоняется native-владельцем.
check(LogManager.getRecent().any { it.category == "PhoneState" && it.level == "WARN" && it.message.contains("not queued") })
drained()
publish(2, 1); applied(2, 1)
for (state in 0..2) for (policy in listOf(0, 1, 2, 4, 8, 16, 31)) {
check(NativeLib.setPhoneState(state, policy)); applied(state, policy)
}
check(!NativeLib.setPhoneState(-1, 0) && !NativeLib.setPhoneState(3, 0))
check(!NativeLib.setPhoneState(0, -1) && !NativeLib.setPhoneState(0, 32))
drained()
check(native("nativeRestart", config))
withTimeout(10_000) { ready.receive() }
publish(2, 1); applied(2, 1)
check(withContext(Dispatchers.IO) { native("nativeStop") })
check(!NativeLib.isReady() && !NativeLib.setPhoneState(2, 1))
publish(2, 1)
nativeInit(directory, logger)
check(NativeLib.start(config))
withTimeout(10_000) { ready.receive() }
publish(2, 1); applied(2, 1)
drained()
check(logger.commands.get() == 25)
} catch (error: Throwable) {
error.printStackTrace(System.err)
throw error
} finally { check(withContext(Dispatchers.IO) { native("nativeDestroy") }) }
println("PASS: Android phone state JNI acceptance, state/policy values, publisher rejection/retry and restart resync")
exitProcess(0)
}
Looper.loop()
}