Compare commits
2
Commits
fa827fdd99
...
6d81f1a28b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d81f1a28b | ||
|
|
2231c38792 |
@@ -44,6 +44,29 @@ class LocalAudioPlaybackTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun electronicCompletionSwitchesToDedicatedClipAndPreservesPrintAudio() {
|
||||||
|
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||||
|
lateinit var service: LocalAudioPlayService
|
||||||
|
instrumentation.runOnMainSync { service = LocalAudioPlayService(instrumentation.targetContext) }
|
||||||
|
try {
|
||||||
|
instrumentation.runOnMainSync { service.playByRoute(AppRoutes.PRINT_SUCCESS) }
|
||||||
|
awaitPlaying(service)
|
||||||
|
assertEquals(R.raw.print_complete_page, service.currentResId.value)
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync {
|
||||||
|
service.playByRoute(AppRoutes.buildElectronicCompletionRoute("AUDIO-TEST-ORDER"))
|
||||||
|
}
|
||||||
|
awaitPlaying(service)
|
||||||
|
assertEquals(R.raw.electronic_complete_page, service.currentResId.value)
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync { service.playByRoute(AppRoutes.PRINT_SUCCESS) }
|
||||||
|
awaitPlaying(service)
|
||||||
|
assertEquals(R.raw.print_complete_page, service.currentResId.value)
|
||||||
|
} finally {
|
||||||
|
instrumentation.runOnMainSync { service.release() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun awaitPlaying(service: LocalAudioPlayService) {
|
private fun awaitPlaying(service: LocalAudioPlayService) {
|
||||||
val deadline = System.currentTimeMillis() + 5000
|
val deadline = System.currentTimeMillis() + 5000
|
||||||
while (!service.isPlaying.value && System.currentTimeMillis() < deadline) Thread.sleep(20)
|
while (!service.isPlaying.value && System.currentTimeMillis() < deadline) Thread.sleep(20)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class RecentPhotosFlowTest {
|
|||||||
req.body!!.writeTo(buffer)
|
req.body!!.writeTo(buffer)
|
||||||
val ids = Gson().fromJson(buffer.readUtf8(), com.google.gson.JsonObject::class.java).getAsJsonArray("image_id")
|
val ids = Gson().fromJson(buffer.readUtf8(), com.google.gson.JsonObject::class.java).getAsJsonArray("image_id")
|
||||||
val amount = "${ids.size() * 2}.00"
|
val amount = "${ids.size() * 2}.00"
|
||||||
val extra = electronicAmount?.let { ",\"price_electronic\":\"$it\",\"amount_electronic\":\"$it\"" }.orEmpty()
|
val extra = electronicAmount?.let { ",\"price_electronic\":\"$it\",\"amount_electronic\":\"${it.toBigDecimal().multiply(ids.size().toBigDecimal()).toPlainString()}\"" }.orEmpty()
|
||||||
"""{"code":100000,"data":{"price_image":"2.00","amount":"$amount"$extra}}"""
|
"""{"code":100000,"data":{"price_image":"2.00","amount":"$amount"$extra}}"""
|
||||||
}
|
}
|
||||||
req.url.encodedPath.endsWith("get-pay-url") && payFails ->
|
req.url.encodedPath.endsWith("get-pay-url") && payFails ->
|
||||||
@@ -467,7 +467,10 @@ class RecentPhotosFlowTest {
|
|||||||
compose.onNodeWithText("二维码加载失败").assertIsDisplayed()
|
compose.onNodeWithText("二维码加载失败").assertIsDisplayed()
|
||||||
qrFails = false
|
qrFails = false
|
||||||
compose.onNodeWithText("重新加载").performClick()
|
compose.onNodeWithText("重新加载").performClick()
|
||||||
compose.waitUntil(10_000) { completionVm!!.qrCodeUrl.value.isNotEmpty() }
|
// URL receipt precedes the background QR bitmap generation.
|
||||||
|
compose.waitUntil(10_000) {
|
||||||
|
compose.onAllNodesWithContentDescription("电子版照片下载二维码").fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
compose.onNodeWithContentDescription("电子版照片下载二维码").assertIsDisplayed()
|
compose.onNodeWithContentDescription("电子版照片下载二维码").assertIsDisplayed()
|
||||||
compose.onNodeWithText("请在屏幕下方拿取照片").assertDoesNotExist()
|
compose.onNodeWithText("请在屏幕下方拿取照片").assertDoesNotExist()
|
||||||
screenshot("electronic-completion")
|
screenshot("electronic-completion")
|
||||||
@@ -479,12 +482,62 @@ class RecentPhotosFlowTest {
|
|||||||
assertEquals(papersBefore, app.appStoreDataSource.getRemainingPaperNum())
|
assertEquals(papersBefore, app.appStoreDataSource.getRemainingPaperNum())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun emptyRecentPhotosStillShowConfiguredUnitPrices() {
|
||||||
|
electronicAmount = "1.00"
|
||||||
|
compose.setContent { FaceRecognitionResultScreen(source = AppRoutes.RECENT_RESULT_SOURCE, viewModel = resultVm) }
|
||||||
|
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.EMPTY }
|
||||||
|
compose.waitForIdle()
|
||||||
|
assertTrue("Entering recent photos must request prices even when the photo list is empty",
|
||||||
|
requests.any { it.url.encodedPath.endsWith("verify-result") })
|
||||||
|
compose.waitUntil(10_000) { !resultVm.quoteLoading.value }
|
||||||
|
compose.onNodeWithText("打印版 2元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("电子版 1元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("购买打印照片\n0元").assertIsNotEnabled()
|
||||||
|
compose.onNodeWithText("购买电子照片\n0元").assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun recentPhotosSelectionUpdatesBothQuotesWithoutReloadReset() {
|
||||||
|
electronicAmount = "1.00"
|
||||||
|
val bitmap = Bitmap.createBitmap(160, 100, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.BLUE) }
|
||||||
|
val photos = (1..2).map { id ->
|
||||||
|
val file = File(context.cacheDir, "recent-price-$id.png")
|
||||||
|
file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||||
|
val url = file.toURI().toString()
|
||||||
|
FaceSearchResult(id, url, url, url, url)
|
||||||
|
}
|
||||||
|
recentBody = Gson().toJson(mapOf("count" to 2, "results" to photos))
|
||||||
|
compose.setContent { FaceRecognitionResultScreen(source = AppRoutes.RECENT_RESULT_SOURCE, viewModel = resultVm) }
|
||||||
|
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.READY && !resultVm.quoteLoading.value }
|
||||||
|
compose.onNodeWithText("打印版 2元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("电子版 1元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("购买电子照片\n0元").assertIsNotEnabled()
|
||||||
|
assertEquals(1, requests.count { it.url.encodedPath.endsWith("verify-result") })
|
||||||
|
compose.runOnIdle { resultVm.toggleSelectAll() }
|
||||||
|
compose.waitUntil(10_000) { !resultVm.quoteLoading.value && resultVm.electronicTotal.value == "2.00" }
|
||||||
|
compose.onNodeWithText("购买打印照片\n4元").assertIsEnabled()
|
||||||
|
compose.onNodeWithText("购买电子照片\n2元").assertIsEnabled()
|
||||||
|
compose.runOnIdle { resultVm.loadRecentPhotos() }
|
||||||
|
compose.waitForIdle()
|
||||||
|
assertEquals("4.00", resultVm.totalPrice.value)
|
||||||
|
assertEquals("2.00", resultVm.electronicTotal.value)
|
||||||
|
assertEquals(2, requests.count { it.url.encodedPath.endsWith("verify-result") })
|
||||||
|
compose.runOnIdle { resultVm.toggleSelectAll() }
|
||||||
|
compose.waitUntil(10_000) { !resultVm.quoteLoading.value && resultVm.totalPrice.value == "0.00" }
|
||||||
|
compose.onNodeWithText("打印版 2元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("电子版 1元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("购买电子照片\n0元").assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun errorCanRetryIntoEmptyState() {
|
@Test fun errorCanRetryIntoEmptyState() {
|
||||||
|
electronicAmount = "1.00"
|
||||||
recentCode = 403
|
recentCode = 403
|
||||||
compose.setContent { FaceRecognitionResultScreen(source = AppRoutes.RECENT_RESULT_SOURCE, viewModel = resultVm) }
|
compose.setContent { FaceRecognitionResultScreen(source = AppRoutes.RECENT_RESULT_SOURCE, viewModel = resultVm) }
|
||||||
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.ERROR }
|
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.ERROR }
|
||||||
compose.onNodeWithText("照片加载失败,请重试").assertIsDisplayed()
|
compose.onNodeWithText("照片加载失败,请重试").assertIsDisplayed()
|
||||||
compose.onNodeWithText("购买打印照片\n--元").assertIsNotEnabled()
|
compose.waitUntil(10_000) { !resultVm.quoteLoading.value }
|
||||||
|
compose.onNodeWithText("打印版 2元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("电子版 1元/张").assertIsDisplayed()
|
||||||
|
compose.onNodeWithText("购买打印照片\n0元").assertIsNotEnabled()
|
||||||
recentCode = 200
|
recentCode = 200
|
||||||
compose.onNodeWithText("重试").performClick()
|
compose.onNodeWithText("重试").performClick()
|
||||||
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.EMPTY }
|
compose.waitUntil(10_000) { resultVm.recentPhotosState.value == RecentPhotosState.EMPTY }
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class LocalAudioPlayService @Inject constructor(
|
|||||||
AppRoutes.PHOTO_SELECT to R.raw.upload_unpaid_result_page,
|
AppRoutes.PHOTO_SELECT to R.raw.upload_unpaid_result_page,
|
||||||
AppRoutes.PRINTING to R.raw.printing_page,
|
AppRoutes.PRINTING to R.raw.printing_page,
|
||||||
AppRoutes.PRINT_SUCCESS to R.raw.print_complete_page,
|
AppRoutes.PRINT_SUCCESS to R.raw.print_complete_page,
|
||||||
|
ELECTRONIC_COMPLETION_AUDIO_KEY to R.raw.electronic_complete_page,
|
||||||
// 特殊状态页面
|
// 特殊状态页面
|
||||||
"face_recognition_recognizing" to R.raw.face_recognition_recognizing_page,
|
"face_recognition_recognizing" to R.raw.face_recognition_recognizing_page,
|
||||||
"face_recognition_failed" to R.raw.face_recognition_failed_page,
|
"face_recognition_failed" to R.raw.face_recognition_failed_page,
|
||||||
@@ -112,10 +113,6 @@ class LocalAudioPlayService @Inject constructor(
|
|||||||
|
|
||||||
// 同一结果页根据入口选择播报,避免最近照片误播识别成功。
|
// 同一结果页根据入口选择播报,避免最近照片误播识别成功。
|
||||||
val audioKey = localAudioKeyForRoute(route)
|
val audioKey = localAudioKeyForRoute(route)
|
||||||
if (audioKey == ELECTRONIC_COMPLETION_AUDIO_KEY) {
|
|
||||||
stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val audioResId = ROUTE_TO_AUDIO_MAP[audioKey]
|
val audioResId = ROUTE_TO_AUDIO_MAP[audioKey]
|
||||||
if (audioResId == null) {
|
if (audioResId == null) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.yzx.kiosk.audio
|
|||||||
import com.yzx.kiosk.navigation.routes.AppRoutes
|
import com.yzx.kiosk.navigation.routes.AppRoutes
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
internal const val ELECTRONIC_COMPLETION_AUDIO_KEY = "electronic_completion_silent"
|
internal const val ELECTRONIC_COMPLETION_AUDIO_KEY = "electronic_completion"
|
||||||
|
|
||||||
internal const val RECENT_PHOTOS_AUDIO_KEY = "recent_photos_result"
|
internal const val RECENT_PHOTOS_AUDIO_KEY = "recent_photos_result"
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ class FaceRecognitionResultViewModel @Inject constructor(
|
|||||||
toPage(AppRoutes.buildRecentPhotosRoute())
|
toPage(AppRoutes.buildRecentPhotosRoute())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadRecentPhotos() = recentLoader.load()
|
fun loadRecentPhotos() {
|
||||||
|
// Pricing belongs to the page, not to a successful/non-empty photo response.
|
||||||
|
if (recentPhotosState.value == RecentPhotosState.IDLE) verifyResult(emptyList())
|
||||||
|
recentLoader.load()
|
||||||
|
}
|
||||||
fun retryRecentPhotos() = recentLoader.load(retry = true)
|
fun retryRecentPhotos() = recentLoader.load(retry = true)
|
||||||
|
|
||||||
// 原始结果列表(用于获取图片id)
|
// 原始结果列表(用于获取图片id)
|
||||||
@@ -177,6 +181,7 @@ class FaceRecognitionResultViewModel @Inject constructor(
|
|||||||
// Returning from a pushed recent-photos page must retain selection, pricing and image state.
|
// Returning from a pushed recent-photos page must retain selection, pricing and image state.
|
||||||
if (faceResultsInitialized) return
|
if (faceResultsInitialized) return
|
||||||
faceResultsInitialized = true
|
faceResultsInitialized = true
|
||||||
|
verifyResult(emptyList())
|
||||||
viewModelScope.launch { applyPhotoList(results) }
|
viewModelScope.launch { applyPhotoList(results) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,8 +220,6 @@ class FaceRecognitionResultViewModel @Inject constructor(
|
|||||||
// 初始化加载状态
|
// 初始化加载状态
|
||||||
_imageLoadStates.value = initialPhotos.associate { it.url to ImageLoadState() }
|
_imageLoadStates.value = initialPhotos.associate { it.url to ImageLoadState() }
|
||||||
|
|
||||||
// 页面初始化时调用验证接口(空列表,type: 2),获取默认价格
|
|
||||||
verifyResult(emptyList())
|
|
||||||
LogUtils.d(TAG, "图片列表初始化完成,共 ${initialPhotos.size} 张,宽高比将在图片加载成功后逐张更新")
|
LogUtils.d(TAG, "图片列表初始化完成,共 ${initialPhotos.size} 张,宽高比将在图片加载成功后逐张更新")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@ import org.junit.Assert.assertEquals
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LocalAudioRouteTest {
|
class LocalAudioRouteTest {
|
||||||
@Test fun `electronic completion suppresses printing audio while print routes retain it`() {
|
@Test fun `electronic completion uses its own audio while print routes retain theirs`() {
|
||||||
assertEquals(ELECTRONIC_COMPLETION_AUDIO_KEY, localAudioKeyForRoute(AppRoutes.buildElectronicCompletionRoute("ORDER")))
|
assertEquals(ELECTRONIC_COMPLETION_AUDIO_KEY, localAudioKeyForRoute(AppRoutes.buildElectronicCompletionRoute("ORDER")))
|
||||||
assertEquals(AppRoutes.PRINTING, localAudioKeyForRoute(AppRoutes.buildPrintingRoute("[]", "ORDER", 1)))
|
assertEquals(AppRoutes.PRINTING, localAudioKeyForRoute(AppRoutes.buildPrintingRoute("[]", "ORDER", 1)))
|
||||||
assertEquals(AppRoutes.PRINTING, localAudioKeyForRoute("printing?urls=electronic%3Dtrue"))
|
assertEquals(AppRoutes.PRINTING, localAudioKeyForRoute("printing?urls=electronic%3Dtrue"))
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# 电子版完成页语音
|
||||||
|
|
||||||
|
- 来源:用户提供的 `9月15日.wav`。
|
||||||
|
- 项目资源名称:`app/src/main/res/raw/electronic_complete_page.wav`。
|
||||||
|
- 原先从打印完成提示音裁剪得到的版本已被替换。
|
||||||
|
- 按原文件复制,未裁剪、转码或调整音量;48 kHz、双声道、16-bit PCM WAV,时长 5.632 秒。
|
||||||
|
- 进入电子版完成页时通过 `LocalAudioPlayService` 播放此资源。
|
||||||
|
- 打印完成页继续使用 `print_complete_page.wav`。
|
||||||
Reference in New Issue
Block a user