Add Android animation library browsing

This commit is contained in:
2026-10-10 14:51:16 +08:00
parent 5db283c05a
commit c9dd03c23b
39 changed files with 1015 additions and 46 deletions
@@ -17,7 +17,7 @@ class FixtureActivity : ComponentActivity() {
val controller = Controller(platform, scope)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { QimiaoApp(controller, controller::scan, { ImageBitmap(64, 64) }) }
setContent { QimiaoApp(controller, controller::scan, { marker -> platform.decodedMarkers += marker; ImageBitmap(64, 64) }) }
}
override fun onDestroy() { controller.disconnect(); scope.cancel(); super.onDestroy() }
}
@@ -28,8 +28,11 @@ class FixturePlatform : MobilePlatform {
override val model = "模拟测试手机"
private val saved = mutableMapOf<String, String>()
val calls = mutableListOf<String>()
val decodedMarkers = java.util.Collections.synchronizedSet(mutableSetOf<String>())
var scanError = false
var emptyLibrary = true
var animatedLibrary = false
override val libraryCache: LibraryCache = FixtureCache()
override fun load(key: String) = saved[key]
override fun save(key: String, value: String) { saved[key] = value }
override fun scan(onDevice: (DiscoveredDevice) -> Unit, onError: (String) -> Unit) {
@@ -59,7 +62,7 @@ private class FixtureLink(val platform: FixturePlatform) : DeviceLink {
val method = request.getValue("method").jsonPrimitive.content
platform.calls += method
val result = when (method) {
"session.open" -> """{"device_id":"00000000-0000-4000-8000-000000000001","device_name":"模拟小屏幕","short_id":"00000000","protocol_major":1,"capabilities":["status","frame","library","library_progressive","settings","wifi","wifi_scan"${if (platform.wifiEnabled) ",\"wifi_transport\"" else ""}]}"""
"session.open" -> """{"device_id":"00000000-0000-4000-8000-000000000001","device_name":"模拟小屏幕","short_id":"00000000","protocol_major":1,"capabilities":["status","frame","library","library_progressive","library_animation","settings","wifi","wifi_scan"${if (platform.wifiEnabled) ",\"wifi_transport\"" else ""}]}"""
"transport.offer" -> """{"device_id":"00000000-0000-4000-8000-000000000001","channel_id":"00000000000000000000000000000000"}"""
"transport.close" -> """{"closed":true}"""
"session.ping" -> """{"alive":true}"""
@@ -68,8 +71,16 @@ private class FixtureLink(val platform: FixturePlatform) : DeviceLink {
"settings.get" -> """{"revision":"fixture","values":{"brightness":40,"orientation":0,"prompt_delay_seconds":5}}"""
"wifi.get" -> """{"revision":"fixture","saved":null,"active":{"connected":false}}"""
"wifi.scan" -> """{"networks":[]}"""
"library.summary" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","item_count":0}""" else """{"library_revision":"fixture-full","item_count":1}"""
"library.list" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","items":[],"next_cursor":null}""" else """{"library_revision":"fixture-full","items":[{"id":"fixture","type":"template","name":"测试模板","revision":"fixture-1","playable":true}],"next_cursor":null}"""
"library.summary" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","item_count":0}""" else if (platform.animatedLibrary) """{"library_revision":"fixture-animation","item_count":1}""" else """{"library_revision":"fixture-full","item_count":1}"""
"library.list" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","items":[],"next_cursor":null}""" else if (platform.animatedLibrary) """{"library_revision":"fixture-animation","items":[{"id":"fixture","type":"animation","name":"测试动图","revision":"fixture-1","frame_count":2,"playable":true}],"next_cursor":null}""" else """{"library_revision":"fixture-full","items":[{"id":"fixture","type":"template","name":"测试模板","revision":"fixture-1","playable":true}],"next_cursor":null}"""
"library.animation.info" -> """{"revision":"fixture-1","frame_count":2,"frames":[{"index":0,"duration_ms":80},{"index":1,"duration_ms":120}],"next_offset":null}"""
"library.animation.chunk" -> {
val params = request.getValue("params").jsonObject
buildJsonObject {
put("revision", "fixture-1"); put("tier", params.getValue("tier")); put("frame_index", params.getValue("frame_index"))
put("offset", 0); put("total_bytes", 1); put("sha256", "fixture"); put("data_base64", "AA=="); put("next_offset", JsonNull)
}.toString()
}
"content.default.get" -> """{"name":"测试默认"}"""
"library.preview" -> """{"data_base64":"fixture-preview"}"""
"library.thumbnail" -> """{"data_base64":"fixture-image"}"""
@@ -112,3 +123,19 @@ class FixtureWifi : WifiPlatform {
override fun close() { replies.close() }
}
}
private class FixtureCache : LibraryCache {
private val images = mutableMapOf<String, String>()
private val records = mutableMapOf<String, JsonObject>()
var reads = 0
override suspend fun loadCatalog(deviceId: String): CachedLibrary? = null
override suspend fun saveCatalog(deviceId: String, catalog: CachedLibrary) {}
override suspend fun readImage(deviceId: String, item: JsonObject, tier: String): String? { reads++; return images[tier] }
override suspend fun writeImage(deviceId: String, item: JsonObject, tier: String, base64: String) { images[tier] = base64 }
override suspend fun readRecord(deviceId: String, item: JsonObject, name: String) = records[name]
override suspend fun writeRecord(deviceId: String, item: JsonObject, name: String, value: JsonObject) { records[name] = value }
override suspend fun writeAnimationFrame(deviceId: String, item: JsonObject, tier: String, chunks: List<String>, sha256: String) { images[tier] = tier }
override suspend fun retain(deviceId: String, items: List<JsonObject>) {}
override suspend fun sizeBytes() = 0L
override suspend fun clear() { images.clear(); records.clear() }
}
@@ -2,6 +2,7 @@ package org.qimiaoscreen.uitest
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.qimiaoscreen.core.thumbnailKey
import org.junit.Rule
import org.junit.Test
import org.junit.Assert.*
@@ -65,6 +66,26 @@ class IsolatedUiTest {
assertTrue(ui.activity.platform.calls.indexOf("library.preview") < ui.activity.platform.calls.indexOf("library.thumbnail"))
assertFalse(ui.activity.platform.calls.contains("content.play"))
}
@Test fun animationViewer() {
ui.runOnIdle { ui.activity.platform.emptyLibrary = false; ui.activity.platform.animatedLibrary = true }
connect(); click("显示内容")
ui.waitUntil(10_000) { ui.activity.controller.state.value.animationClips.values.any { it.tier == "full" } }
ui.waitUntil(5_000) { ui.activity.platform.decodedMarkers.containsAll(listOf("animation-full-0", "animation-full-1")) }
val item = ui.activity.controller.state.value.library.single()
ui.onNodeWithTag("library_image_" + thumbnailKey(item)).performScrollTo().performClick()
ui.onNodeWithTag("image_viewer").assertExists()
ui.waitUntil(5_000) { ui.activity.controller.state.value.viewerProgress == null }
ui.onNodeWithTag("viewer_image").assertExists()
ui.onNodeWithTag("viewer_close").performClick()
ui.onNodeWithTag("image_viewer").assertDoesNotExist()
ui.runOnIdle { ui.activity.controller.foreground(false) }
assertFalse(ui.activity.controller.state.value.imagesForeground)
ui.runOnIdle { ui.activity.controller.foreground(true) }
ui.onNodeWithTag("frame_image").performScrollTo().performClick()
ui.onNodeWithTag("image_viewer").assertExists()
ui.onNodeWithTag("viewer_close").performClick()
assertFalse(ui.activity.platform.calls.contains("content.play"))
}
@Test fun settings() {
connect(); click("设备设置")
ui.waitUntil(5_000) { ui.activity.controller.state.value.settings != null }