| @@ -1,6 +1,9 @@ |
| 1 | 1 | package dev.craft.navigationtest |
| 2 | 2 | |
| 3 | 3 | import android.content.Intent |
| 4 | +import android.media.AudioManager |
| 5 | +import android.media.MediaPlayer |
| 6 | +import androidx.test.core.app.ActivityScenario |
| 4 | 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 |
| 5 | 8 | import androidx.test.platform.app.InstrumentationRegistry |
| 6 | 9 | import java.net.ServerSocket |
| @@ -43,6 +46,100 @@ class NativeCapabilitiesTest { |
| 43 | 46 | |
| 44 | 47 | private val instrumentation = InstrumentationRegistry.getInstrumentation() |
| 45 | 48 | |
| 49 | + |
| 50 | + |
| 51 | + private fun silentWav(): java.io.File { |
| 52 | + val samples = 8000 * 2 |
| 53 | + val bytes = java.nio.ByteBuffer.allocate(44 + samples * 2).order(java.nio.ByteOrder.LITTLE_ENDIAN) |
| 54 | + bytes.put("RIFF".toByteArray()).putInt(36 + samples * 2).put("WAVEfmt ".toByteArray()) |
| 55 | + bytes.putInt(16).putShort(1.toShort()).putShort(1.toShort()).putInt(8000).putInt(16000) |
| 56 | + bytes.putShort(2.toShort()).putShort(16.toShort()).put("data".toByteArray()).putInt(samples * 2) |
| 57 | + return java.io.File(instrumentation.targetContext.cacheDir, "sound-${UUID.randomUUID()}.wav").apply { writeBytes(bytes.array()) } |
| 58 | + } |
| 59 | + |
| 60 | + private fun sound(service: CraftNativeSounds): Any = |
| 61 | + (CraftNativeSounds::class.java.getDeclaredField("sounds").apply { isAccessible = true }.get(service) as Map<*, *>)["tone"]!! |
| 62 | + |
| 63 | + private fun field(sound: Any, name: String): Any? = |
| 64 | + sound.javaClass.getDeclaredField(name).apply { isAccessible = true }.get(sound) |
| 65 | + |
| 66 | + private fun focus(service: CraftNativeSounds, change: Int) { |
| 67 | + CraftNativeSounds::class.java.getDeclaredMethod("onAudioFocusChange", Int::class.javaPrimitiveType) |
| 68 | + .apply { isAccessible = true }.invoke(service, change) |
| 69 | + } |
| 70 | + |
| 71 | + @Test |
| 72 | + fun replacingAPreparingSoundSettlesItAndItsOldCancellationCannotStopTheReplacement() { |
| 73 | + val file = silentWav() |
| 74 | + try { |
| 75 | + ActivityScenario.launch(MainActivity::class.java).use { scenario -> |
| 76 | + lateinit var service: CraftNativeSounds |
| 77 | + val ready = CountDownLatch(1) |
| 78 | + var replacement: CraftNativeCapabilityResult? = null |
| 79 | + var cancelled = 0 |
| 80 | + try { |
| 81 | + scenario.onActivity { activity -> |
| 82 | + service = CraftNativeSounds(activity) |
| 83 | + service.play("tone", file.toURI().toString(), JSONObject()) { |
| 84 | + assertEquals("CANCELLED", it.code) |
| 85 | + cancelled++ |
| 86 | + } |
| 87 | + val oldCancel = service.cancellation("tone") |
| 88 | + service.play("tone", file.toURI().toString(), JSONObject().put("loop", true)) { |
| 89 | + replacement = it |
| 90 | + ready.countDown() |
| 91 | + } |
| 92 | + assertEquals(1, cancelled) |
| 93 | + oldCancel() |
| 94 | + } |
| 95 | + assertTrue("replacement did not prepare", ready.await(10, TimeUnit.SECONDS)) |
| 96 | + assertNull(replacement?.message, replacement?.code) |
| 97 | + scenario.onActivity { |
| 98 | + assertTrue((field(sound(service), "player") as MediaPlayer).isPlaying) |
| 99 | + assertEquals(1, cancelled) |
| 100 | + } |
| 101 | + } finally { scenario.onActivity { service.close() } } |
| 102 | + } |
| 103 | + } finally { file.delete() } |
| 104 | + } |
| 105 | + |
| 106 | + @Test |
| 107 | + fun explicitPauseSurvivesFocusGainAndVolumeChangesStayCurrent() { |
| 108 | + val file = silentWav() |
| 109 | + try { |
| 110 | + ActivityScenario.launch(MainActivity::class.java).use { scenario -> |
| 111 | + lateinit var service: CraftNativeSounds |
| 112 | + val ready = CountDownLatch(1) |
| 113 | + var result: CraftNativeCapabilityResult? = null |
| 114 | + try { |
| 115 | + scenario.onActivity { activity -> |
| 116 | + service = CraftNativeSounds(activity) |
| 117 | + service.play("tone", file.toURI().toString(), JSONObject().put("loop", true).put("volume", 0.8).put("fadeIn", 1000)) { |
| 118 | + result = it |
| 119 | + ready.countDown() |
| 120 | + } |
| 121 | + assertEquals(0f, field(sound(service), "volume")) |
| 122 | + } |
| 123 | + assertTrue("sound did not prepare", ready.await(10, TimeUnit.SECONDS)) |
| 124 | + assertNull(result?.message, result?.code) |
| 125 | + scenario.onActivity { |
| 126 | + assertTrue(service.setVolume("tone", 0.25, 0.0)) |
| 127 | + assertEquals(0.25f, field(sound(service), "volume")) |
| 128 | + focus(service, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) |
| 129 | + focus(service, AudioManager.AUDIOFOCUS_GAIN) |
| 130 | + assertEquals(0.25f, field(sound(service), "volume")) |
| 131 | + focus(service, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) |
| 132 | + assertTrue(service.pause("tone")) |
| 133 | + focus(service, AudioManager.AUDIOFOCUS_GAIN) |
| 134 | + assertFalse((field(sound(service), "player") as MediaPlayer).isPlaying) |
| 135 | + assertTrue(service.resume("tone")) |
| 136 | + assertTrue((field(sound(service), "player") as MediaPlayer).isPlaying) |
| 137 | + } |
| 138 | + } finally { scenario.onActivity { service.close() } } |
| 139 | + } |
| 140 | + } finally { file.delete() } |
| 141 | + } |
| 142 | + |
| 46 | 143 | @Test |
| 47 | 144 | fun rejectedSnapshotReplacementPreservesDiskCacheAndTheOtherIsolates() { |
| 48 | 145 | val base = instrumentation.targetContext |