package link.roadio.riff.ui import android.os.Looper import androidx.test.platform.app.InstrumentationRegistry import link.roadio.riff.player.library.PlayerCuetreeClient import link.roadio.riff.storage.root.ApplicationDataStorage import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger /** Exercise production scheduling, source replacement and the no-UI-wait boundary. */ class BrowsePreparationInstrumentedTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() private class SlowRows : BrowserRows() { val entered = CountDownLatch(1) val release = CountDownLatch(1) val mainKeys = AtomicInteger() override val size = 1000 override fun key(index: Int): BrowserRowKey { if (Looper.myLooper() == Looper.getMainLooper()) mainKeys.incrementAndGet() if (index == 0) { entered.countDown() check(release.await(10, TimeUnit.SECONDS)) } return BrowserRowKey("album:$index", "Album ${1000 - index}", BrowserKind.ALBUM) } override fun get(index: Int) = BrowserItem("album:$index", "Album ${1000 - index}", BrowserKind.ALBUM) } @Test fun albumPreparationDoesNotBlockMainAndPublishesOnlyTheCurrentSource() { val context = instrumentation.targetContext check(context.packageName.endsWith(".instrumented")) check(ApplicationDataStorage(context).prepareForStartup()) UiConfig.load(context) val slow = SlowRows() val category = BrowserItem("category:albums", "Albums", BrowserKind.CATEGORY, children = slow) val root = BrowserItem("root", "Library", BrowserKind.ROOT, children = listOf(category)).withBrowseNamePolicy(false) var controller: RiffPlayerUiController? = null try { onMain { controller = RiffPlayerUiController(context).also { (field(it, "libraryClient").get(it) as PlayerCuetreeClient).close() field(it, "state").set(it, it.state.copy(root = root, browserPath = emptyList(), screen = PlayerScreen.MAIN)) it.dispatch(PlayerUiAction.OpenCategory("category:albums")) } } assertTrue(slow.entered.await(5, TimeUnit.SECONDS)) onMain { assertEquals(0, controller!!.state.visibleChildren().size) assertTrue(!controller!!.state.azAppliedReady()) // A replacement source invalidates the captured query while old preparation is blocked. val replacement = BrowserItem( "root", "Library", BrowserKind.ROOT, children = listOf(category.copy(children = listOf(BrowserItem("album:new", "New", BrowserKind.ALBUM)))), ).withBrowseNamePolicy(false) field( controller!!, "state", ).set(controller, controller!!.state.copy(root = replacement, screen = PlayerScreen.MAIN, browserPath = emptyList())) controller!!.dispatch(PlayerUiAction.OpenCategory("category:albums")) } slow.release.countDown() val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) var ready = false while (!ready && System.nanoTime() < deadline) { onMain { ready = controller!!.state.currentAzSnapshot() != null } if (!ready) Thread.sleep(20) } assertTrue("Replacement browse source was not prepared", ready) onMain { assertEquals(listOf("New"), controller!!.state.visibleChildren().map { it.label }) } assertEquals("Sort/A-Z requested metadata on main", 0, slow.mainKeys.get()) } finally { slow.release.countDown() onMain { controller?.close() } } } private fun field( controller: RiffPlayerUiController, name: String, ) = controller.javaClass.getDeclaredField(name).apply { isAccessible = true } private fun onMain(action: () -> Unit) = instrumentation.runOnMainSync(action) }