package link.roadio.riff.ui import android.os.SystemClock import androidx.test.platform.app.InstrumentationRegistry import link.roadio.riff.player.library.PlayerCuetreeClient import link.roadio.riff.player.storage.PlayerProcessStore import link.roadio.riff.storage.root.ApplicationDataLocation import link.roadio.riff.storage.root.ApplicationDataSelection import link.roadio.riff.storage.root.ApplicationDataStorage import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue import org.junit.Test import java.io.File /** Opt-in real-volume fixture, confined to the disposable package's application-data roots. */ class UserStateRootFenceInstrumentedTest { @Test fun persistenceOnlyConnectionDoesNotObserveCatalogAndRepeatedFenceCanResume() { assumeTrue(InstrumentationRegistry.getArguments().getString("riffRootFenceProbe") == "true") val context = InstrumentationRegistry.getInstrumentation().targetContext check(context.packageName == "link.roadio.riff.instrumented") assumeTrue(context.contentResolver.persistedUriPermissions.none { it.isReadPermission }) val storage = ApplicationDataStorage(context) assertTrue(storage.prepareForStartup()) val catalog = File(storage.activeRoot(), "indexer/cuetree") val before = inventory(catalog) checkNotNull(PlayerProcessStore.open(context, 15)) PlayerProcessStore.closeForRootChange() val client = PlayerCuetreeClient(context, {}, {}, {}) try { assertTrue(client.isIndexerBound()) assertTrue(client.quiesceForStorageMigration()) assertTrue(client.quiesceForStorageMigration()) assertEquals(before, inventory(catalog)) val selected = storage.selection() assertTrue(client.activateStorageRootAfterMigration(selected.location.name, selected.portableVolumeId, false)) val store = checkNotNull(PlayerProcessStore.open(context, 15)) val settings = store.loadSettings(selected.location.toPlayer()) store.saveSettings(settings.copy(autoSearch = !settings.autoSearch)) store.awaitSaved() store.saveSettings(settings) store.awaitSaved() } finally { client.close() PlayerProcessStore.closeForRootChange() } } @Test fun controllerMovesCompleteFlatBufferStateToSdAndBackWithCatalogUiDisabled() { assumeTrue(InstrumentationRegistry.getArguments().getString("riffRootFenceProbe") == "true") val instrumentation = InstrumentationRegistry.getInstrumentation() val context = instrumentation.targetContext check(context.packageName == "link.roadio.riff.instrumented") assumeTrue(context.contentResolver.persistedUriPermissions.none { it.isReadPermission }) val storage = ApplicationDataStorage(context) assertTrue(storage.prepareForStartup()) assertEquals(ApplicationDataLocation.HU, storage.selection().location) val portable = storage.eligiblePortableRoots().firstOrNull() assumeTrue("A writable portable volume is required", portable != null) val sd = ApplicationDataSelection(ApplicationDataLocation.SD, checkNotNull(portable).volumeId) val originalMode = storage.cachedApplicationMode() storage.cacheApplicationMode("phone") val originalStore = checkNotNull(PlayerProcessStore.open(context, 15)) val settings = originalStore.loadSettings(ApplicationDataLocation.HU.toPlayer()) originalStore.saveSettings(settings.copy(autoSearch = !settings.autoSearch)) originalStore.awaitSaved() var controller: RiffPlayerUiController? = null fun move(target: ApplicationDataSelection) { instrumentation.runOnMainSync { val method = RiffPlayerUiController::class.java.getDeclaredMethod("migrateStorage", ApplicationDataSelection::class.java) method.isAccessible = true method.invoke(checkNotNull(controller), target) } val deadline = SystemClock.elapsedRealtime() + 90_000 var status = "" while (SystemClock.elapsedRealtime() < deadline) { instrumentation.runOnMainSync { status = checkNotNull(controller).state.status } if (status.startsWith("Application data moved to")) { assertEquals(target, storage.selection()) val reopened = PlayerProcessStore.current() assertNotNull(reopened) assertEquals(!settings.autoSearch, checkNotNull(reopened).loadSettings(target.location.toPlayer()).autoSearch) return } check(!status.startsWith("Data move failed:")) { status } SystemClock.sleep(25) } error("Data move did not finish: $status") } try { instrumentation.runOnMainSync { controller = RiffPlayerUiController(context) } move(sd) // A fresh controller also verifies that cold admission reads the copied root. instrumentation.runOnMainSync { controller!!.close() controller = RiffPlayerUiController(context) } move(ApplicationDataSelection(ApplicationDataLocation.HU)) } finally { instrumentation.runOnMainSync { controller?.close() } if (storage.selection().location == ApplicationDataLocation.SD) { val client = PlayerCuetreeClient(context, {}, {}, {}) try { PlayerProcessStore.closeForRootChange() check(client.quiesceForStorageMigration()) storage.migrateTo(ApplicationDataSelection(ApplicationDataLocation.HU)) check(client.activateStorageRootAfterMigration("HU", null, false)) } finally { client.close() } } PlayerProcessStore.open(context, 15)?.let { restored -> restored.saveSettings(settings) restored.awaitSaved() } storage.cacheApplicationMode(originalMode) PlayerProcessStore.closeForRootChange() } } private fun inventory(root: File): Map> = root.walkTopDown().filter(File::isFile).associate { it.relativeTo(root).path to it.readBytes().toList() } private fun ApplicationDataLocation.toPlayer() = if (this == ApplicationDataLocation.HU) { link.roadio.riff.player.core.settings.PlayerDataLocation.HU } else { link.roadio.riff.player.core.settings.PlayerDataLocation.SD } }